diff --git a/.forgejo/renovate.json b/.forgejo/renovate.json new file mode 100644 index 000000000..009ebf659 --- /dev/null +++ b/.forgejo/renovate.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "renovate/config" + ], + "enabledManagers": ["nuget", "github-actions"], + "packageRules": [ + { + // require approval for *all* NuGet package updates, not just major versions. + "matchDepTypes": "nuget", + "dependencyDashboardApproval": true + }, + { + // Ignore Gommon for automatic updates. I make breaking changes on minor updates not infrequently. + "matchDepNames": "Gommon", + "matchDepTypes": "nuget", + "enabled": false + }, + { + "description": "group Silk.NET packages", + "extends": ["renovate/config//groups/silkdotnet.json"], + "groupName": "Silk.NET" + }, + { + "description": "group OpenTK packages", + "extends": ["renovate/config//groups/opentk.json"], + "groupName": "OpenTK" + }, + { + "description": "group Svg.Controls packages", + "extends": ["renovate/config//groups/svgcontrols.json"], + "groupName": "Svg.Controls" + } + ] +} diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index a7db99713..e7052a6f6 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -35,9 +35,9 @@ jobs: fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@v5 with: global-json-file: global.json @@ -94,7 +94,7 @@ jobs: shell: bash - name: Upload Ryujinx Windows artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ryujinx-${{ matrix.configuration }}-${{ steps.version_info.outputs.result }}+${{ steps.version_info.outputs.git_short_hash }}-${{ matrix.platform.zip_os_name }} path: artifact @@ -133,7 +133,7 @@ jobs: shell: bash - name: Upload Ryujinx AppImage artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 if: forgejo.event_name == 'pull_request' && contains(matrix.platform.name, 'linux') with: name: ryujinx-${{ matrix.configuration }}-${{ steps.version_info.outputs.result }}+${{ steps.version_info.outputs.git_short_hash }}-${{ matrix.platform.zip_os_name }}-AppImage @@ -148,9 +148,9 @@ jobs: configuration: [ Release ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@v5 with: global-json-file: global.json @@ -197,7 +197,7 @@ jobs: shell: bash - name: Upload Ryujinx artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ryujinx-${{ matrix.configuration }}-${{ steps.version_info.outputs.result }}+${{ steps.version_info.outputs.git_short_hash }}-macos_universal path: "publish/*.tar.gz" diff --git a/.forgejo/workflows/canary.yml b/.forgejo/workflows/canary.yml index 4cb4cdcd5..930e6b253 100644 --- a/.forgejo/workflows/canary.yml +++ b/.forgejo/workflows/canary.yml @@ -217,7 +217,7 @@ jobs: - macos_release - release steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install GLI uses: actions/setup-gli@v1 diff --git a/.forgejo/workflows/pr_triage.yml b/.forgejo/workflows/pr_triage.yml index 1b17c31c8..2608bc8f2 100644 --- a/.forgejo/workflows/pr_triage.yml +++ b/.forgejo/workflows/pr_triage.yml @@ -10,7 +10,7 @@ jobs: steps: # Grab sources to get latest labeler.yml - name: Fetch sources - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: # Ensure we pin the source origin as pull_request_target run under forks. fetch-depth: 0 diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 45123c2cd..341430adb 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -143,9 +143,7 @@ jobs: macos_release: name: Release MacOS universal - runs-on: docker - container: - image: ghcr.io/catthehacker/ubuntu:act-latest + runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 @@ -207,12 +205,12 @@ jobs: post_ci: name: Post-CI Steps - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest needs: - macos_release - release steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install GLI uses: actions/setup-gli@v1 diff --git a/.forgejo/workflows/unused/nightly_pr_comment.yml b/.forgejo/workflows/unused/nightly_pr_comment.yml deleted file mode 100644 index 24d23d98b..000000000 --- a/.forgejo/workflows/unused/nightly_pr_comment.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Comment PR artifacts links - -on: - workflow_run: - workflows: ['Build PR'] - types: [completed] - -jobs: - pr_comment: - if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' - runs-on: ubuntu-latest - steps: - - uses: actions/github-script@v6 - with: - script: | - const {owner, repo} = context.repo; - const run_id = ${{github.event.workflow_run.id}}; - const pull_head_sha = '${{github.event.workflow_run.head_sha}}'; - - const issue_number = await (async () => { - const pulls = await github.rest.pulls.list({owner, repo}); - for await (const {data} of github.paginate.iterator(pulls)) { - for (const pull of data) { - if (pull.head.sha === pull_head_sha) { - return pull.number; - } - } - } - })(); - if (issue_number) { - core.info(`Using pull request ${issue_number}`); - } else { - return core.error(`No matching pull request found`); - } - - const {data: {artifacts}} = await github.rest.actions.listWorkflowRunArtifacts({owner, repo, run_id}); - if (!artifacts.length) { - return core.error(`No artifacts found`); - } - let body = `Download the artifacts for this pull request:\n`; - let hidden_debug_artifacts = `\n\n
Only for Developers\n`; - for (const art of artifacts) { - const url = `https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip`; - if (art.name.includes('Debug')) { - hidden_debug_artifacts += `\n* [${art.name}](${url})`; - } else { - body += `\n* [${art.name}](${url})`; - } - } - hidden_debug_artifacts += `\n
`; - body += hidden_debug_artifacts; - - const {data: comments} = await github.rest.issues.listComments({repo, owner, issue_number}); - const existing_comment = comments.find((c) => c.user.login === 'github-actions[bot]'); - if (existing_comment) { - core.info(`Updating comment ${existing_comment.id}`); - await github.rest.issues.updateComment({repo, owner, comment_id: existing_comment.id, body}); - } else { - core.info(`Creating a comment`); - await github.rest.issues.createComment({repo, owner, issue_number, body}); - } diff --git a/Directory.Packages.props b/Directory.Packages.props index 014235f95..8c5ce0410 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,59 +3,66 @@ true - + - - - - - - + + + + + + - - + + - + - - + + - - + + - - - - - - + + + + + + + + - - - - + + + + + + - + - + - - - + + + + - + + + - - + + diff --git a/README.md b/README.md index ce18233c0..0364dc7fa 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ -# Ryujinx +

Ryujinx

[![Latest release](https://git.ryujinx.app/projects/Ryubing/badges/release.svg?label=stable&color=32cd32)](https://update.ryujinx.app/latest/stable) [![Latest canary release](https://git.ryujinx.app/Ryubing/Canary/badges/release.svg?label=canary&color=FF4500)](https://update.ryujinx.app/latest/canary) @@ -21,7 +21,7 @@ Ryujinx is an open-source Nintendo Switch emulator, originally created by gdkchan, written in C#. This emulator aims at providing excellent accuracy and performance, a user-friendly interface and consistent builds. It was written from scratch and development on the project began in September 2017. - Ryujinx is available on a self-managed modified Forgejo instance under the MIT license. + Ryujinx is available on a self-managed modified Forgejo instance under the MIT license.

diff --git a/assets/Locales/Root.json b/assets/Locales/Root.json index a11aad25a..d9139904c 100644 --- a/assets/Locales/Root.json +++ b/assets/Locales/Root.json @@ -596,7 +596,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "重启模拟", "zh_TW": "重新啟動模擬" } }, @@ -6100,6 +6100,31 @@ "zh_TW": "檔案系統全域存取日誌模式:" } }, + { + "ID": "SettingsTabLoggingEnableNetLogs", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Enable Net Logs", + "es_ES": "Habilitar Registros de Red.", + "fr_FR": "Activer les Journaux Réseau.", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "开启网络日志", + "zh_TW": "" + } + }, { "ID": "SettingsTabLoggingDeveloperOptions", "Translations": { @@ -11346,7 +11371,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "保存", "zh_TW": "儲存" } }, @@ -12225,6 +12250,56 @@ "zh_TW": "弱震動調節:" } }, + { + "ID": "ControllerSettingsRumbleUseHDRumble", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Enable HD Rumble", + "es_ES": "Activa vibración HD", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "HDRumbleTooltip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Sends more data to the controller for better rumble.\n\nCurrently only supports first-party Nintendo Switch controllers.\n\nLeave ON if you're using JoyCons or a Pro Controller.", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, { "ID": "DialogMessageSaveNotAvailableMessage", "Translations": { @@ -17075,6 +17150,31 @@ "zh_TW": "啟用檔案系統存取日誌輸出到控制台中。可能的模式為 0 到 3。" } }, + { + "ID": "NetLogTooltip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Prints network log messages in the console.", + "es_ES": "Imprimir registros de red en la consola.", + "fr_FR": "Affiche les journaux réseau dans la console.", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "在控制台中显示网络日志", + "zh_TW": "" + } + }, { "ID": "DeveloperOptionTooltip", "Translations": { @@ -21425,6 +21525,31 @@ "zh_TW": "需要重新啟動 Ryujinx" } }, + { + "ID": "SettingsShowConsoleRestartMessage", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "The console will be available the next time Ryujinx starts.", + "es_ES": "La consola estará disponible la próxima vez que se inicie Ryujinx.", + "fr_FR": "La console sera disponible au prochain démarrage de Ryujinx.", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "控制台将会在下次启动 Ryujinx 时可用。", + "zh_TW": "" + } + }, { "ID": "SettingsGpuBackendRestartMessage", "Translations": { diff --git a/distribution/macos/construct_universal_dylib.py b/distribution/macos/construct_universal_dylib.py index 5d9321860..18b84399f 100644 --- a/distribution/macos/construct_universal_dylib.py +++ b/distribution/macos/construct_universal_dylib.py @@ -47,14 +47,12 @@ def get_new_name( input_component = str(input_dylib_path).replace(str(input_directory), "")[1:] return Path(os.path.join(output_directory, input_component)) - -def is_fat_file(dylib_path: Path) -> str: - res = subprocess.check_output([LIPO, "-info", str(dylib_path.absolute())]).decode( - "utf-8" - ) - - return not res.split("\n")[0].startswith("Non-fat file") - +def get_archs(dylib_path: Path) -> list[str]: + res = subprocess.check_output([LIPO, "-info", str(dylib_path)]).decode("utf-8") + if res.startswith("Non-fat file"): + return [res.split(":")[-1].strip()] + else: + return res.split("are:")[-1].strip().split() def construct_universal_dylib( arm64_input_dylib_path: Path, x86_64_input_dylib_path: Path, output_dylib_path: Path @@ -69,11 +67,12 @@ def construct_universal_dylib( os.path.basename(arm64_input_dylib_path.resolve()), output_dylib_path ) else: - if is_fat_file(arm64_input_dylib_path) or not x86_64_input_dylib_path.exists(): - with open(output_dylib_path, "wb") as dst: - with open(arm64_input_dylib_path, "rb") as src: - dst.write(src.read()) - else: + arm64_archs = get_archs(arm64_input_dylib_path) + x86_64_archs = get_archs(x86_64_input_dylib_path) if x86_64_input_dylib_path.exists() else [] + + if "arm64" in arm64_archs and "x86_64" in arm64_archs: + shutil.copy2(arm64_input_dylib_path, output_dylib_path) + elif x86_64_archs: subprocess.check_call( [ LIPO, diff --git a/docs/compatibility.csv b/docs/compatibility.csv index fa804c909..a0a7e730a 100644 --- a/docs/compatibility.csv +++ b/docs/compatibility.csv @@ -1061,6 +1061,7 @@ 0100BCA016636000,"eBaseball Powerful Pro Yakyuu 2022",gpu;services-horizon;crash,nothing,2024-05-26 23:07:19 01001F20100B8000,"Eclipse: Edge of Light",,playable,2020-08-11 23:06:29 0100E0A0110F4000,"eCrossminton",,playable,2020-07-11 18:24:27 +010054601D54C000,"Emio – The Smiling Man: Famicom Detective Club (DEMO)",demo,playable,2026-05-13 18:32:12 0100ABE00DB4E000,"Edna & Harvey: Harvey's New Eyes",nvdec,playable,2021-01-26 14:36:08 01004F000B716000,"Edna & Harvey: The Breakout – Anniversary Edition",crash;nvdec,ingame,2022-08-01 16:59:56 01002550129F0000,"Effie",,playable,2022-10-27 14:36:39 @@ -1204,7 +1205,7 @@ 01003B200E440000,"Five Nights at Freddy's: Sister Location",,playable,2023-10-06 09:00:58 010038200E088000,"Flan",crash;regression,ingame,2021-11-17 07:39:28 01000A0004C50000,"FLASHBACK™",nvdec,playable,2020-05-14 13:57:29 -0100C53004C52000,"Flat Heroes",gpu,ingame,2022-07-26 19:37:37 +0100C53004C52000,"Flat Heroes",,playable,2026-02-27 17:00:00 0100B54012798000,"Flatland: Prologue",,playable,2020-12-11 20:41:12 0100307004B4C000,"Flinthook",online,playable,2021-03-25 20:42:29 010095A004040000,"Flip Wars",services;ldn-untested,ingame,2022-05-02 15:39:18 @@ -1394,6 +1395,7 @@ 0100c3c012718000,"Grand Theft Auto: III – The Definitive Edition",gpu;UE4,ingame,2022-10-31 20:13:52 0100182014022000,"Grand Theft Auto: Vice City – The Definitive Edition",gpu;UE4,ingame,2022-10-31 20:13:52 010065a014024000,"Grand Theft Auto: San Andreas – The Definitive Edition",gpu;UE4,ingame,2022-10-31 20:13:52 +0100EB500D92E000,"GROOVE COASTER WAI WAI PARTY!!!!",gpu,ingame,2026-05-13 18:32:12 0100822012D76000,"HAAK",gpu,ingame,2023-02-19 14:31:05 01007E100EFA8000,"Habroxia",,playable,2020-06-16 23:04:42 0100535012974000,"Hades",vulkan,playable,2022-10-05 10:45:21 @@ -1832,6 +1834,7 @@ 010055200E87E000,"Metamorphosis",UE4;audout;gpu;nvdec,ingame,2021-06-16 16:18:11 0100D4900E82C000,"Metro 2033 Redux",gpu,ingame,2022-11-09 10:53:13 0100F0400E850000,"Metro: Last Light Redux",slow;nvdec;vulkan-backend-bug,ingame,2023-11-01 11:53:52 +010019A01E2F2000,"Metroid Prime 4: Beyond",,ingame,2026-05-13 18:32:12 010012101468C000,"Metroid Prime™ Remastered",gpu;Needs Update;vulkan-backend-bug;opengl-backend-bug,ingame,2024-05-07 22:48:15 010093801237C000,"Metroid™ Dread",,playable,2023-11-13 04:02:36 0100A1200F20C000,"Midnight Evil",,playable,2022-10-18 22:55:19 @@ -1945,6 +1948,7 @@ 0100C3E00ACAA000,"Mutant Football League: Dynasty Edition",online-broken,playable,2022-08-05 17:01:51 01004BE004A86000,"Mutant Mudds Collection",,playable,2022-08-05 17:11:38 0100E6B00DEA4000,"Mutant Year Zero: Road to Eden - Deluxe Edition",nvdec;UE4,playable,2022-09-10 13:31:10 +010037501F864000,"Mute Crimson DX",,ingame,2026-05-13 18:32:12 0100161009E5C000,"MX Nitro: Unleashed",,playable,2022-09-27 22:34:33 0100218011E7E000,"MX vs ATV All Out",nvdec;UE4;vulkan-backend-bug,playable,2022-10-25 19:51:46 0100D940063A0000,"MXGP3 - The Official Motocross Videogame",UE4;gpu;nvdec,ingame,2020-12-16 14:00:20 @@ -2268,6 +2272,7 @@ 010086F0064CE000,"Poi: Explorer Edition",nvdec,playable,2021-01-21 19:32:00 0100EB6012FD2000,"Poison Control",,playable,2021-05-16 14:01:54 010072400E04A000,"Pokémon Café ReMix",,playable,2021-08-17 20:00:04 +01005B7008C52800,"Pokémon Champions",Needs Update;services;online-broke,menus,2026-05-13 18:32:12 010008c01e742000,"Pokémon Friends",crash;services,menus,2025-07-24 13:32:00 01003D200BAA2000,"Pokémon Mystery Dungeon™: Rescue Team DX",mac-bug,playable,2024-01-21 00:16:32 01008DB008C2C000,"Pokémon Shield + Pokémon Shield Expansion Pass",deadlock;crash;online-broken;ldn-works;LAN,ingame,2024-08-12 07:20:22 @@ -2275,6 +2280,8 @@ 01009AD008C4C000,"Pokémon: Let's Go, Pikachu! demo",slow;demo,playable,2023-11-26 11:23:20 0100000011D90000,"Pokémon™ Brilliant Diamond",gpu;ldn-works,ingame,2024-08-28 13:26:35 010018E011D92000,"Pokémon™ Shining Pearl",gpu;ldn-works,ingame,2024-08-28 13:26:35 +100554023408000,"Pokémon FireRed Version",crashes,nothing,2026-05-13 18:32:12 +010034D02340E000,"Pokémon LeafGreen Version",crashes,nothing,2026-05-13 18:32:12 010015F008C54000,"Pokémon™ HOME",Needs Update;crash;services,menus,2020-12-06 06:01:51 01001F5010DFA000,"Pokémon™ Legends: Arceus",gpu;Needs Update;ldn-works,ingame,2024-09-19 10:02:02 0100F43008C44000,"Pokémon™ Legends: Z-A",gpu;crash;ldn-works,ingame,2025-11-16 00:30:00 @@ -2866,7 +2873,7 @@ 0100277011F1A000,"Super Mario Bros.™ 35",online-broken,menus,2022-08-07 16:27:25 010015100B514000,"Super Mario Bros.™ Wonder",amd-vendor-bug,playable,2024-09-06 13:21:21 01009B90006DC000,"Super Mario Maker™ 2",online-broken;ldn-broken,playable,2024-08-25 11:05:19 -0100000000010000,"Super Mario Odyssey™",nvdec;intel-vendor-bug;mac-bug,playable,2024-08-25 01:32:34 +0100000000010000,"Super Mario Odyssey™",nvdec;intel-vendor-bug;mac-bug;amd-vendor-bug,playable,2026-05-13 18:32:12 010036B0034E4000,"Super Mario Party™",gpu;Needs Update;ldn-works,ingame,2024-06-21 05:10:16 0100965017338000,"Super Mario Party Jamboree",mac-bug;gpu,ingame,2025-02-17 02:09:20 0100BC0018138000,"Super Mario RPG™",gpu;audio;nvdec,ingame,2024-06-19 17:43:42 @@ -3163,6 +3170,8 @@ 0100E2E00CB14000,"Tokyo School Life",,playable,2022-09-16 20:25:54 010024601BB16000,"Tomb Raider I-III Remastered Starring Lara Croft",gpu;opengl,ingame,2024-09-27 12:32:04 0100D7F01E49C000,"Tomba! Special Edition",services-horizon,nothing,2024-09-15 21:59:54 +010051F0207B2000,"Tomodachi Life: Living the Dream",amd-vendor-bug;gpu;intel-vendor-bug;ldn-broken,ingame,2026-05-13 18:32:12 +0100CA502552A000,"Tomodachi Life: Living the Dream – Welcome Edtion",amd-vendor-bug;demo,playable,2026-05-13 18:32:12 0100D400100F8000,"Tonight We Riot",,playable,2021-02-26 15:55:09 0100CC00102B4000,"Tony Hawk's™ Pro Skater™ 1 + 2",gpu;Needs Update,ingame,2024-09-24 08:18:14 010093F00E818000,"Tools Up!",crash,ingame,2020-07-21 12:58:17 diff --git a/src/Ryujinx.Common/Configuration/Hid/Controller/RumbleConfigController.cs b/src/Ryujinx.Common/Configuration/Hid/Controller/RumbleConfigController.cs index ee8ab457d..f190996c1 100644 --- a/src/Ryujinx.Common/Configuration/Hid/Controller/RumbleConfigController.cs +++ b/src/Ryujinx.Common/Configuration/Hid/Controller/RumbleConfigController.cs @@ -16,5 +16,10 @@ namespace Ryujinx.Common.Configuration.Hid.Controller /// Enable Rumble /// public bool EnableRumble { get; set; } + + ///

+ /// Enable HD Rumble support + /// OperatingSystem.IsWindows(); + public static bool HasConsoleWindow => OperatingSystem.IsWindows() && GetConsoleWindow() != nint.Zero; public static void SetConsoleWindowState(bool show) { @@ -33,18 +34,31 @@ namespace Ryujinx.Common.Helper [SupportedOSPlatform("windows")] private static void SetConsoleWindowStateWindows(bool show) { - const int SW_HIDE = 0; - const int SW_SHOW = 5; - - nint hWnd = GetConsoleWindow(); - - if (hWnd == nint.Zero) + if (show) { - Logger.Warning?.Print(LogClass.Application, "Attempted to show/hide console window but console window does not exist"); + if (GetConsoleWindow() != nint.Zero) + { + Logger.SetConsoleTargetEnabled(true); + } return; } - ShowWindow(hWnd, show ? SW_SHOW : SW_HIDE); + Logger.SetConsoleTargetEnabled(false); + DetachConsole(); + } + + [SupportedOSPlatform("windows")] + private static void DetachConsole() + { + if (GetConsoleWindow() == nint.Zero) + { + return; + } + + if (!FreeConsole()) + { + Logger.Warning?.Print(LogClass.Application, "Attempted to detach console window but the operation failed"); + } } } } diff --git a/src/Ryujinx.Common/Logging/LogClass.cs b/src/Ryujinx.Common/Logging/LogClass.cs index 7c6810599..0e02cfcc2 100644 --- a/src/Ryujinx.Common/Logging/LogClass.cs +++ b/src/Ryujinx.Common/Logging/LogClass.cs @@ -51,6 +51,7 @@ namespace Ryujinx.Common.Logging ServiceNgct, ServiceNifm, ServiceNim, + ServiceNotification, ServiceNs, ServiceNsd, ServiceNtc, diff --git a/src/Ryujinx.Common/Logging/LogLevel.cs b/src/Ryujinx.Common/Logging/LogLevel.cs index 282b07111..e8778191a 100644 --- a/src/Ryujinx.Common/Logging/LogLevel.cs +++ b/src/Ryujinx.Common/Logging/LogLevel.cs @@ -12,6 +12,7 @@ namespace Ryujinx.Common.Logging Error, Guest, AccessLog, + NetLog, Notice, Trace, } diff --git a/src/Ryujinx.Common/Logging/Logger.cs b/src/Ryujinx.Common/Logging/Logger.cs index 64a76a3e4..788d2ab3d 100644 --- a/src/Ryujinx.Common/Logging/Logger.cs +++ b/src/Ryujinx.Common/Logging/Logger.cs @@ -119,6 +119,7 @@ namespace Ryujinx.Common.Logging public static Log? Error { get; private set; } public static Log? Guest { get; private set; } public static Log? AccessLog { get; private set; } + public static Log? NetLog { get; private set; } public static Log? Stub { get; private set; } public static Log? Trace { get; private set; } public static Log Notice { get; } // Always enabled @@ -136,11 +137,7 @@ namespace Ryujinx.Common.Logging _time = Stopwatch.StartNew(); - // Logger should log to console by default - AddTarget(new AsyncLogTargetWrapper( - new ConsoleLogTarget("console"), - 1000, - AsyncLogTargetOverflowAction.Discard)); + SetConsoleTargetEnabled(true); Notice = new Log(LogLevel.Notice); @@ -173,6 +170,21 @@ namespace Ryujinx.Common.Logging Updated += target.Log; } + public static void SetConsoleTargetEnabled(bool enabled) + { + if (enabled) + { + AddTarget(new AsyncLogTargetWrapper( + new ConsoleLogTarget("console"), + 1000, + AsyncLogTargetOverflowAction.Discard)); + } + else + { + RemoveTarget("console"); + } + } + public static void RemoveTarget(string target) { ILogTarget logTarget = GetTarget(target); @@ -236,6 +248,7 @@ namespace Ryujinx.Common.Logging case LogLevel.Error : Error = enabled ? new Log(LogLevel.Error) : null; break; case LogLevel.Guest : Guest = enabled ? new Log(LogLevel.Guest) : null; break; case LogLevel.AccessLog : AccessLog = enabled ? new Log(LogLevel.AccessLog) : null; break; + case LogLevel.NetLog : NetLog = enabled ? new Log(LogLevel.NetLog) : null; break; case LogLevel.Stub : Stub = enabled ? new Log(LogLevel.Stub) : null; break; case LogLevel.Trace : Trace = enabled ? new Log(LogLevel.Trace) : null; break; case LogLevel.Notice : break; diff --git a/src/Ryujinx.Common/TitleIDs.cs b/src/Ryujinx.Common/TitleIDs.cs index c232cfd01..890cf2793 100644 --- a/src/Ryujinx.Common/TitleIDs.cs +++ b/src/Ryujinx.Common/TitleIDs.cs @@ -59,6 +59,7 @@ namespace Ryujinx.Common //Mario Franchise "010021d00812a000", // Arcade Archives VS. SUPER MARIO BROS. + "01007fe0221d8000", // Hello, Mario! "01006d0017f7a000", // Mario & Luigi: Brothership "010003000e146000", // Mario & Sonic at the Olympic Games Tokyo 2020 "010067300059a000", // Mario + Rabbids: Kingdom Battle @@ -70,6 +71,9 @@ namespace Ryujinx.Common "0100bde00862a000", // Mario Tennis Aces "0100b99019412000", // Mario vs. Donkey Kong "010049900f546000", // Super Mario 3D All-Stars + "010049900f546001", // Super Mario 3D All-Stars | Super Mario 64 + "010049900f546002", // Super Mario 3D All-Stars | Super Mario Sunshine + "010049900f546003", // Super Mario 3D All-Stars | Super Mario Galaxy "010028600ebda000", // Super Mario 3D World + Bowser's Fury "010049900F546001", // Super Mario 64 "0100ea80032ea000", // Super Mario Bros. U Deluxe @@ -107,6 +111,11 @@ namespace Ryujinx.Common "0100187003a36000", // Pokémon: Let's Go Eevee! "010003f003a34000", // Pokémon: Let's Go Pikachu! "0100f43008c44000", // Pokémon Legends: Z-A + "0100554023408000", // Pokémon FireRed Version (EN) + "01006fa0233f8000", // Pokémon FireRed Version (JP) + "0100fd6023430000", // Pokémon LeafGreen Version (DE) + "0100f1e0233fa000", // Pokémon LeafGreen Version (JP) + "01005b7008c52000", // Pokémon Champions //Splatoon Franchise "0100f8f0000a2000", // Splatoon 2 (EU) @@ -116,13 +125,14 @@ namespace Ryujinx.Common "0100ba0018500000", // Splatoon 3: Splatfest World Premiere //NSO Membership games + "0100d870045b6000", // NES - Nintendo Switch Online + "01008d300c50c000", // SNES - Nintendo Switch Online "0100c62011050000", // GB - Nintendo Switch Online "010012f017576000", // GBA - Nintendo Switch Online "0100c9a00ece6000", // N64 - Nintendo Switch Online "0100e0601c632000", // N64 - Nintendo Switch Online 18+ - "0100d870045b6000", // NES - Nintendo Switch Online "0100b3c014bda000", // SEGA Genesis - Nintendo Switch Online - "01008d300c50c000", // SNES - Nintendo Switch Online + "0100bfc01d976000", // Virtual Boy - Nintendo Switch Online "0100ccf019c8c000", // F-ZERO 99 "0100ad9012510000", // PAC-MAN 99 "010040600c5ce000", // Tetris 99 @@ -141,12 +151,17 @@ namespace Ryujinx.Common "0100704000B3A000", // Snipperclips "01006a800016e000", // Super Smash Bros. Ultimate "0100a9400c9c2000", // Tokyo Mirage Sessions #FE Encore + "0100ca502552a000", // Tomodachi Life: Living the Dream - Welcome Edition + "010051f0207b2000", // Tomodachi Life: Living the Dream //Bayonetta Franchise "010076f0049a2000", // Bayonetta "01007960049a0000", // Bayonetta 2 "01004a4010fea000", // Bayonetta 3 "0100cf5010fec000", // Bayonetta Origins: Cereza and the Lost Demon + + // Famicom Detective Club Franchise + "010054601d54c000", // Emio - The Smiling Man: Famicom Detective Series (DEMO) //Persona Franchise "0100dcd01525a000", // Persona 3 Portable @@ -171,7 +186,9 @@ namespace Ryujinx.Common "0100453019aa8000", // Xenoblade Chronicles: X Definitive Edition //Misc Games + "01003670066de000", // 36 Fragments of Midnight "010056e00853a000", // A Hat in Time + "0100c9f00aaee000", // Ascendence "0100fd1014726000", // Baldurs Gate: Dark Alliance "01008c2019598000", // Bluey: The Video Game "010096f00ff22000", // Borderlands 2: Game of the Year Edition @@ -185,8 +202,10 @@ namespace Ryujinx.Common "010027400cdc6000", // Divinity Original 2 - Definitive Edition "01008c8012920000", // Dying Light Platinum Edition "0100d11013e6a000", // Eschatos + "01000490067ae000", // Frederic 2: Evil Strikes Back "01001cc01b2d4000", // Goat Simulator 3 "01003620068ea000", // Hand of Fate 2 + "01007ac00e012000", // HEXAGRAVITY "0100f7e00c70e000", // Hogwarts Legacy "010013c00e930000", // Hollow Knight: Silksong "010085500130a000", // Lego City: Undercover @@ -196,6 +215,7 @@ namespace Ryujinx.Common "0100853015e86000", // No Man's Sky "0100f85014ed0000", // No More Heroes "0100463014ed4000", // No More Heroes 2 + "0100f7d00a1bc000", // NO THING "0100e570094e8000", // Owlboy "01007bb017812000", // Portal "0100abd01785c000", // Portal 2 @@ -204,11 +224,14 @@ namespace Ryujinx.Common "01008e200c5c2000", // Muse Dash "01005ff002e2a000", // Rayman Legends "01007820196a6000", // Red Dead Redemption + "01007a800d520000", // REFUNCT "0100e8300a67a000", // Risk "01002f7013224000", // Rune Factory 5 "01008d100d43e000", // Saints Row IV "0100de600beee000", // Saints Row: The Third - The Full Package "01001180021fa000", // Shovel Knight: Specter of Torment + "010079f00671c000", // Sparkle 2: Evo + "010077b00e046000", // Spyro: Reignited Trilogy "0100e1D01eb2e000", // Squeakross: Home Squeak Home "0100e65002bb8000", // Stardew Valley "0100d7a01b7a2000", // Star Wars: Bounty Hunter diff --git a/src/Ryujinx.Cpu/LightningJit/Cache/JitCache.cs b/src/Ryujinx.Cpu/LightningJit/Cache/JitCache.cs index 4ab54ecb7..a96e3554a 100644 --- a/src/Ryujinx.Cpu/LightningJit/Cache/JitCache.cs +++ b/src/Ryujinx.Cpu/LightningJit/Cache/JitCache.cs @@ -17,7 +17,8 @@ namespace Ryujinx.Cpu.LightningJit.Cache private static readonly int _pageMask = _pageSize - 1; private const int CodeAlignment = 4; // Bytes. - private const int CacheSize = 256 * 1024 * 1024; + // TODO: JIT Cache size should be application dependent, not global. + private const int CacheSize = 1024 * (1024 * 1024); // Megabytes * Size of Megabytes (since its in bytes). private static JitCacheInvalidation _jitCacheInvalidator; @@ -33,6 +34,14 @@ namespace Ryujinx.Cpu.LightningJit.Cache [SupportedOSPlatform("windows")] [LibraryImport("kernel32.dll", SetLastError = true)] public static partial nint FlushInstructionCache(nint hProcess, nint lpAddress, nuint dwSize); + + [SupportedOSPlatform("macos")] + [LibraryImport("libSystem.dylib", EntryPoint = "sys_icache_invalidate")] + internal static partial void SysICacheInvalidate(nint start, nuint len); + + [SupportedOSPlatform("linux")] + [LibraryImport("libgcc_s.so.1", EntryPoint = "__clear_cache")] + internal static partial void ClearCache(nint begin, nint end); public static void Initialize(IJitMemoryAllocator allocator) { diff --git a/src/Ryujinx.Graphics.GAL/Capabilities.cs b/src/Ryujinx.Graphics.GAL/Capabilities.cs index 1eec80e51..4271c3d18 100644 --- a/src/Ryujinx.Graphics.GAL/Capabilities.cs +++ b/src/Ryujinx.Graphics.GAL/Capabilities.cs @@ -42,6 +42,7 @@ namespace Ryujinx.Graphics.GAL public readonly bool SupportsShaderBallot; public readonly bool SupportsShaderBarrierDivergence; public readonly bool SupportsShaderFloat64; + public readonly bool SupportsShaderNonUniformIndexing; public readonly bool SupportsTextureGatherOffsets; public readonly bool SupportsTextureShadowLod; public readonly bool SupportsVertexStoreAndAtomics; @@ -110,6 +111,7 @@ namespace Ryujinx.Graphics.GAL bool supportsShaderBallot, bool supportsShaderBarrierDivergence, bool supportsShaderFloat64, + bool supportsShaderNonUniformIndexing, bool supportsTextureGatherOffsets, bool supportsTextureShadowLod, bool supportsVertexStoreAndAtomics, @@ -172,6 +174,7 @@ namespace Ryujinx.Graphics.GAL SupportsShaderBallot = supportsShaderBallot; SupportsShaderBarrierDivergence = supportsShaderBarrierDivergence; SupportsShaderFloat64 = supportsShaderFloat64; + SupportsShaderNonUniformIndexing = supportsShaderNonUniformIndexing; SupportsTextureGatherOffsets = supportsTextureGatherOffsets; SupportsTextureShadowLod = supportsTextureShadowLod; SupportsVertexStoreAndAtomics = supportsVertexStoreAndAtomics; diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs index 6444b18e3..7b58e18c3 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs @@ -22,7 +22,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache private const ushort FileFormatVersionMajor = 1; private const ushort FileFormatVersionMinor = 2; private const uint FileFormatVersionPacked = ((uint)FileFormatVersionMajor << 16) | FileFormatVersionMinor; - private const uint CodeGenVersion = 7353; + private const uint CodeGenVersion = 7354; private const string SharedTocFileName = "shared.toc"; private const string SharedDataFileName = "shared.data"; diff --git a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs index d89eebabf..2f8c329e5 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs @@ -231,6 +231,8 @@ namespace Ryujinx.Graphics.Gpu.Shader public bool QueryHostSupportsShaderFloat64() => _context.Capabilities.SupportsShaderFloat64; + public bool QueryHostSupportsShaderNonUniformIndexing() => _context.Capabilities.SupportsShaderNonUniformIndexing; + public bool QueryHostSupportsSnormBufferTextureFormat() => _context.Capabilities.SupportsSnormBufferTextureFormat; public bool QueryHostSupportsTextureGatherOffsets() => _context.Capabilities.SupportsTextureGatherOffsets; diff --git a/src/Ryujinx.Graphics.OpenGL/Image/TextureView.cs b/src/Ryujinx.Graphics.OpenGL/Image/TextureView.cs index 12ec23c8b..9b1df80dc 100644 --- a/src/Ryujinx.Graphics.OpenGL/Image/TextureView.cs +++ b/src/Ryujinx.Graphics.OpenGL/Image/TextureView.cs @@ -551,7 +551,7 @@ namespace Ryujinx.Graphics.OpenGL.Image level, x, width, - format.PixelFormat, + (InternalFormat) format.PixelFormat, mipSize, data); } @@ -579,7 +579,7 @@ namespace Ryujinx.Graphics.OpenGL.Image layer, width, 1, - format.PixelFormat, + (InternalFormat) format.PixelFormat, mipSize, data); } @@ -609,7 +609,7 @@ namespace Ryujinx.Graphics.OpenGL.Image y, width, height, - format.PixelFormat, + (InternalFormat) format.PixelFormat, mipSize, data); } @@ -643,7 +643,7 @@ namespace Ryujinx.Graphics.OpenGL.Image width, height, 1, - format.PixelFormat, + (InternalFormat) format.PixelFormat, mipSize, data); } @@ -675,7 +675,7 @@ namespace Ryujinx.Graphics.OpenGL.Image y, width, height, - format.PixelFormat, + (InternalFormat) format.PixelFormat, mipSize, data); } @@ -744,7 +744,7 @@ namespace Ryujinx.Graphics.OpenGL.Image level, 0, width, - format.PixelFormat, + (InternalFormat) format.PixelFormat, mipSize, data); } @@ -773,7 +773,7 @@ namespace Ryujinx.Graphics.OpenGL.Image 0, width, height, - format.PixelFormat, + (InternalFormat) format.PixelFormat, mipSize, data); } @@ -807,7 +807,7 @@ namespace Ryujinx.Graphics.OpenGL.Image width, height, depth, - format.PixelFormat, + (InternalFormat) format.PixelFormat, mipSize, data); } @@ -843,7 +843,7 @@ namespace Ryujinx.Graphics.OpenGL.Image 0, width, height, - format.PixelFormat, + (InternalFormat) format.PixelFormat, mipSize / 6, data + faceOffset); } diff --git a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs index af1494bbe..acc0dbd68 100644 --- a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs +++ b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs @@ -184,6 +184,7 @@ namespace Ryujinx.Graphics.OpenGL supportsShaderBallot: HwCapabilities.SupportsShaderBallot, supportsShaderBarrierDivergence: !(intelWindows || intelUnix), supportsShaderFloat64: true, + supportsShaderNonUniformIndexing: false, supportsTextureGatherOffsets: true, supportsTextureShadowLod: HwCapabilities.SupportsTextureShadowLod, supportsVertexStoreAndAtomics: true, diff --git a/src/Ryujinx.Graphics.OpenGL/PersistentBuffers.cs b/src/Ryujinx.Graphics.OpenGL/PersistentBuffers.cs index d5c02f4df..b77ac5f17 100644 --- a/src/Ryujinx.Graphics.OpenGL/PersistentBuffers.cs +++ b/src/Ryujinx.Graphics.OpenGL/PersistentBuffers.cs @@ -27,7 +27,7 @@ namespace Ryujinx.Graphics.OpenGL public void Map(BufferHandle handle, int size) { GL.BindBuffer(BufferTarget.CopyWriteBuffer, handle); - nint ptr = GL.MapBufferRange(BufferTarget.CopyWriteBuffer, nint.Zero, size, BufferAccessMask.MapReadBit | BufferAccessMask.MapPersistentBit); + nint ptr = GL.MapBufferRange(BufferTarget.CopyWriteBuffer, nint.Zero, size, MapBufferAccessMask.MapReadBit | MapBufferAccessMask.MapPersistentBit); _maps[handle] = ptr; } @@ -75,7 +75,7 @@ namespace Ryujinx.Graphics.OpenGL GL.BindBuffer(BufferTarget.CopyWriteBuffer, _copyBufferHandle); GL.BufferStorage(BufferTarget.CopyWriteBuffer, requiredSize, nint.Zero, BufferStorageFlags.MapReadBit | BufferStorageFlags.MapPersistentBit); - _bufferMap = GL.MapBufferRange(BufferTarget.CopyWriteBuffer, nint.Zero, requiredSize, BufferAccessMask.MapReadBit | BufferAccessMask.MapPersistentBit); + _bufferMap = GL.MapBufferRange(BufferTarget.CopyWriteBuffer, nint.Zero, requiredSize, MapBufferAccessMask.MapReadBit | MapBufferAccessMask.MapPersistentBit); } } diff --git a/src/Ryujinx.Graphics.OpenGL/Pipeline.cs b/src/Ryujinx.Graphics.OpenGL/Pipeline.cs index e58e6f2b9..5b1e63e3b 100644 --- a/src/Ryujinx.Graphics.OpenGL/Pipeline.cs +++ b/src/Ryujinx.Graphics.OpenGL/Pipeline.cs @@ -924,8 +924,8 @@ namespace Ryujinx.Graphics.OpenGL GL.Disable(EnableCap.CullFace); return; } - - GL.CullFace(face.Convert()); + + GL.CullFace((TriangleFace) face.Convert()); GL.Enable(EnableCap.CullFace); } @@ -1085,12 +1085,12 @@ namespace Ryujinx.Graphics.OpenGL { if (frontMode == backMode) { - GL.PolygonMode(MaterialFace.FrontAndBack, frontMode.Convert()); + GL.PolygonMode((TriangleFace) MaterialFace.FrontAndBack, frontMode.Convert()); } else { - GL.PolygonMode(MaterialFace.Front, frontMode.Convert()); - GL.PolygonMode(MaterialFace.Back, backMode.Convert()); + GL.PolygonMode((TriangleFace) MaterialFace.Front, frontMode.Convert()); + GL.PolygonMode((TriangleFace) MaterialFace.Back, backMode.Convert()); } } diff --git a/src/Ryujinx.Graphics.OpenGL/Program.cs b/src/Ryujinx.Graphics.OpenGL/Program.cs index 608a03451..cb9933c10 100644 --- a/src/Ryujinx.Graphics.OpenGL/Program.cs +++ b/src/Ryujinx.Graphics.OpenGL/Program.cs @@ -59,7 +59,7 @@ namespace Ryujinx.Graphics.OpenGL GL.CompileShader(shaderHandle); break; case TargetLanguage.Spirv: - GL.ShaderBinary(1, ref shaderHandle, (BinaryFormat)All.ShaderBinaryFormatSpirVArb, shader.BinaryCode, shader.BinaryCode.Length); + GL.ShaderBinary(1, ref shaderHandle, ShaderBinaryFormat.ShaderBinaryFormatSpirV, shader.BinaryCode, shader.BinaryCode.Length); GL.SpecializeShader(shaderHandle, "main", 0, (int[])null, (int[])null); break; } diff --git a/src/Ryujinx.Graphics.OpenGL/Queries/BufferedQuery.cs b/src/Ryujinx.Graphics.OpenGL/Queries/BufferedQuery.cs index f39829923..c9dbcdcf2 100644 --- a/src/Ryujinx.Graphics.OpenGL/Queries/BufferedQuery.cs +++ b/src/Ryujinx.Graphics.OpenGL/Queries/BufferedQuery.cs @@ -32,7 +32,7 @@ namespace Ryujinx.Graphics.OpenGL.Queries GL.BufferStorage(BufferTarget.QueryBuffer, sizeof(long), (nint)(&defaultValue), BufferStorageFlags.MapReadBit | BufferStorageFlags.MapWriteBit | BufferStorageFlags.MapPersistentBit); } - _bufferMap = GL.MapBufferRange(BufferTarget.QueryBuffer, nint.Zero, sizeof(long), BufferAccessMask.MapReadBit | BufferAccessMask.MapWriteBit | BufferAccessMask.MapPersistentBit); + _bufferMap = GL.MapBufferRange(BufferTarget.QueryBuffer, nint.Zero, sizeof(long), MapBufferAccessMask.MapReadBit | MapBufferAccessMask.MapWriteBit | MapBufferAccessMask.MapPersistentBit); } public void Reset() diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs index 4fe214778..3f80c2ae0 100644 --- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs @@ -82,6 +82,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv public bool IsMainFunction { get; private set; } public bool MayHaveReturned { get; set; } + public bool WasNonUniformAccessDeclared { get; set; } public CodeGenContext( StructuredProgramInfo info, @@ -89,6 +90,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv GeneratorPool instPool, GeneratorPool integerPool) : base(SpirvVersionPacked, instPool, integerPool) { + WasNonUniformAccessDeclared = false; + Info = info; AttributeUsage = parameters.AttributeUsage; Definitions = parameters.Definitions; diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs index 83b037c1c..77a23d1f2 100644 --- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs @@ -587,6 +587,23 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv return OperationResult.Invalid; } + private static void MarkNonUniform(CodeGenContext context, SpvInstruction inst) + { + if (context.HostCapabilities.SupportsShaderNonUniformIndexing) + { + if (!context.WasNonUniformAccessDeclared) + { + context.AddExtension("SPV_EXT_descriptor_indexing"); + context.AddCapability(Capability.ShaderNonUniform); + context.AddCapability(Capability.SampledImageArrayNonUniformIndexing); + context.AddCapability(Capability.StorageImageArrayNonUniformIndexing); + } + + context.Decorate(inst, Decoration.NonUniform); + context.WasNonUniformAccessDeclared = true; + } + } + private static OperationResult GenerateImageAtomic(CodeGenContext context, AstOperation operation) { AstTextureOperation texOp = (AstTextureOperation)operation; @@ -613,6 +630,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv SpvInstruction textureIndex = Src(AggregateType.S32); image = context.AccessChain(imagePointerType, image, textureIndex); + MarkNonUniform(context, image); } int coordsCount = texOp.Type.Dimensions; @@ -683,15 +701,21 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv ImageDeclaration declaration = context.Images[texOp.GetTextureSetAndBinding()]; SpvInstruction image = declaration.Image; + bool isIndexed = declaration.IsIndexed; - if (declaration.IsIndexed) + if (isIndexed) { SpvInstruction textureIndex = Src(AggregateType.S32); image = context.AccessChain(declaration.ImagePointerType, image, textureIndex); + MarkNonUniform(context, image); } image = context.Load(declaration.ImageType, image); + if (isIndexed) + { + MarkNonUniform(context, image); + } int coordsCount = texOp.Type.Dimensions; @@ -740,15 +764,21 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv ImageDeclaration declaration = context.Images[texOp.GetTextureSetAndBinding()]; SpvInstruction image = declaration.Image; + bool isIndexed = declaration.IsIndexed; - if (declaration.IsIndexed) + if (isIndexed) { SpvInstruction textureIndex = Src(AggregateType.S32); image = context.AccessChain(declaration.ImagePointerType, image, textureIndex); + MarkNonUniform(context, image); } image = context.Load(declaration.ImageType, image); + if (isIndexed) + { + MarkNonUniform(context, image); + } int coordsCount = texOp.Type.Dimensions; @@ -1878,35 +1908,56 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv private static SpvInstruction GenerateSampledImageLoad(CodeGenContext context, AstTextureOperation texOp, SamplerDeclaration declaration, ref int srcIndex) { SpvInstruction image = declaration.Image; + bool imageIndexed = declaration.IsIndexed; - if (declaration.IsIndexed) + if (imageIndexed) { SpvInstruction textureIndex = context.Get(AggregateType.S32, texOp.GetSource(srcIndex++)); image = context.AccessChain(declaration.SampledImagePointerType, image, textureIndex); + MarkNonUniform(context, image); } if (texOp.IsSeparate) { image = context.Load(declaration.ImageType, image); + if (imageIndexed) + { + MarkNonUniform(context, image); + } SamplerDeclaration samplerDeclaration = context.Samplers[texOp.GetSamplerSetAndBinding()]; SpvInstruction sampler = samplerDeclaration.Image; + bool samplerIndexed = samplerDeclaration.IsIndexed; - if (samplerDeclaration.IsIndexed) + if (samplerIndexed) { SpvInstruction samplerIndex = context.Get(AggregateType.S32, texOp.GetSource(srcIndex++)); sampler = context.AccessChain(samplerDeclaration.SampledImagePointerType, sampler, samplerIndex); + MarkNonUniform(context, sampler); } sampler = context.Load(samplerDeclaration.ImageType, sampler); + if (samplerIndexed) + { + MarkNonUniform(context, sampler); + } + image = context.SampledImage(declaration.SampledImageType, image, sampler); + if (imageIndexed || samplerIndexed) + { + MarkNonUniform(context, image); + } } else { image = context.Load(declaration.SampledImageType, image); + if (imageIndexed) + { + MarkNonUniform(context, image); + } } return image; diff --git a/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs b/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs index 4e6d6edf9..77953df05 100644 --- a/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs +++ b/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs @@ -336,6 +336,10 @@ namespace Ryujinx.Graphics.Shader { return true; } + bool QueryHostSupportsShaderNonUniformIndexing() + { + return false; + } /// /// Queries host GPU support for signed normalized buffer texture formats. diff --git a/src/Ryujinx.Graphics.Shader/Translation/HostCapabilities.cs b/src/Ryujinx.Graphics.Shader/Translation/HostCapabilities.cs index 11fe6599d..bfd85c158 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/HostCapabilities.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/HostCapabilities.cs @@ -9,6 +9,7 @@ namespace Ryujinx.Graphics.Shader.Translation public readonly bool SupportsShaderBallot; public readonly bool SupportsShaderBarrierDivergence; public readonly bool SupportsShaderFloat64; + public readonly bool SupportsShaderNonUniformIndexing; public readonly bool SupportsTextureShadowLod; public readonly bool SupportsViewportMask; @@ -20,6 +21,7 @@ namespace Ryujinx.Graphics.Shader.Translation bool supportsShaderBallot, bool supportsShaderBarrierDivergence, bool supportsShaderFloat64, + bool supportsShaderNonUniformIndexing, bool supportsTextureShadowLod, bool supportsViewportMask) { @@ -30,6 +32,7 @@ namespace Ryujinx.Graphics.Shader.Translation SupportsShaderBallot = supportsShaderBallot; SupportsShaderBarrierDivergence = supportsShaderBarrierDivergence; SupportsShaderFloat64 = supportsShaderFloat64; + SupportsShaderNonUniformIndexing = supportsShaderNonUniformIndexing; SupportsTextureShadowLod = supportsTextureShadowLod; SupportsViewportMask = supportsViewportMask; } diff --git a/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs b/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs index e1ca22610..c7c562947 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs @@ -364,6 +364,7 @@ namespace Ryujinx.Graphics.Shader.Translation GpuAccessor.QueryHostSupportsShaderBallot(), GpuAccessor.QueryHostSupportsShaderBarrierDivergence(), GpuAccessor.QueryHostSupportsShaderFloat64(), + GpuAccessor.QueryHostSupportsShaderNonUniformIndexing(), GpuAccessor.QueryHostSupportsTextureShadowLod(), GpuAccessor.QueryHostSupportsViewportMask()); diff --git a/src/Ryujinx.Graphics.Vulkan/Auto.cs b/src/Ryujinx.Graphics.Vulkan/Auto.cs index 7ce309a5d..d97d69ad3 100644 --- a/src/Ryujinx.Graphics.Vulkan/Auto.cs +++ b/src/Ryujinx.Graphics.Vulkan/Auto.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common.Logging; using System; using System.Diagnostics; using System.Threading; @@ -114,7 +115,7 @@ namespace Ryujinx.Graphics.Vulkan cbs.AddDependant(this); // We need to add a dependency on the command buffer to all objects this object - // references aswell. + // references as well. if (_referencedObjs != null) { for (int i = 0; i < _referencedObjs.Length; i++) @@ -175,7 +176,7 @@ namespace Ryujinx.Graphics.Vulkan } } } - + Debug.Assert(_referenceCount >= 0); } diff --git a/src/Ryujinx.Graphics.Vulkan/BarrierBatch.cs b/src/Ryujinx.Graphics.Vulkan/BarrierBatch.cs index 251f74319..927845fa0 100644 --- a/src/Ryujinx.Graphics.Vulkan/BarrierBatch.cs +++ b/src/Ryujinx.Graphics.Vulkan/BarrierBatch.cs @@ -46,7 +46,14 @@ namespace Ryujinx.Graphics.Vulkan public static (AccessFlags Access, PipelineStageFlags Stages) GetSubpassAccessSuperset(VulkanRenderer gd) { - AccessFlags access = BufferAccess; + AccessFlags access = BufferAccess | + AccessFlags.ShaderReadBit | + AccessFlags.ShaderWriteBit | + AccessFlags.ColorAttachmentReadBit | + AccessFlags.ColorAttachmentWriteBit | + AccessFlags.DepthStencilAttachmentReadBit | + AccessFlags.DepthStencilAttachmentWriteBit; + PipelineStageFlags stages = PipelineStageFlags.AllGraphicsBit; if (gd.TransformFeedbackApi != null) diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineLayoutFactory.cs b/src/Ryujinx.Graphics.Vulkan/PipelineLayoutFactory.cs index 612a8b25d..b285e57f5 100644 --- a/src/Ryujinx.Graphics.Vulkan/PipelineLayoutFactory.cs +++ b/src/Ryujinx.Graphics.Vulkan/PipelineLayoutFactory.cs @@ -16,15 +16,15 @@ namespace Ryujinx.Graphics.Vulkan { DescriptorSetLayout[] layouts = new DescriptorSetLayout[setDescriptors.Count]; bool[] updateAfterBindFlags = new bool[setDescriptors.Count]; - + bool isMoltenVk = gd.IsMoltenVk; - + for (int setIndex = 0; setIndex < setDescriptors.Count; setIndex++) { ResourceDescriptorCollection rdc = setDescriptors[setIndex]; ResourceStages activeStages = ResourceStages.None; - + if (isMoltenVk) { for (int descIndex = 0; descIndex < rdc.Descriptors.Count; descIndex++) @@ -42,12 +42,13 @@ namespace Ryujinx.Graphics.Vulkan ResourceDescriptor descriptor = rdc.Descriptors[descIndex]; ResourceStages stages = descriptor.Stages; - if (descriptor.Type == ResourceType.StorageBuffer && isMoltenVk) + if (descriptor.Type == ResourceType.StorageBuffer && gd.IsMoltenVk) { - // There's a bug on MoltenVK where using the same buffer across different stages + // There's a bug in MoltenVK where using the same buffer across different stages // causes invalid resource errors, allow the binding on all active stages as workaround. + // https://github.com/KhronosGroup/MoltenVK/issues/1870 stages = activeStages; - } + } layoutBindings[descIndex] = new DescriptorSetLayoutBinding { diff --git a/src/Ryujinx.Graphics.Vulkan/TextureCopy.cs b/src/Ryujinx.Graphics.Vulkan/TextureCopy.cs index aae3b0afb..0efb4dbb0 100644 --- a/src/Ryujinx.Graphics.Vulkan/TextureCopy.cs +++ b/src/Ryujinx.Graphics.Vulkan/TextureCopy.cs @@ -15,6 +15,8 @@ namespace Ryujinx.Graphics.Vulkan Image dstImage, TextureCreateInfo srcInfo, TextureCreateInfo dstInfo, + TextureCreateInfo srcStorageInfo, + TextureCreateInfo dstStorageInfo, Extents2D srcRegion, Extents2D dstRegion, int srcLayer, @@ -40,6 +42,13 @@ namespace Ryujinx.Graphics.Vulkan return (xy1, xy2); } + static (Offset3D, Offset3D) ClampOffsetsToMip(Offset3D xy1, Offset3D xy2, int mipW, int mipH) + { + return ( + new Offset3D(Math.Min(xy1.X, mipW), Math.Min(xy1.Y, mipH), xy1.Z), + new Offset3D(Math.Min(xy2.X, mipW), Math.Min(xy2.Y, mipH), xy2.Z)); + } + if (srcAspectFlags == 0) { srcAspectFlags = srcInfo.Format.ConvertAspectFlags(); @@ -80,6 +89,14 @@ namespace Ryujinx.Graphics.Vulkan (srcOffsets.Element0, srcOffsets.Element1) = ExtentsToOffset3D(srcRegion, srcInfo.Width, srcInfo.Height, level); (dstOffsets.Element0, dstOffsets.Element1) = ExtentsToOffset3D(dstRegion, dstInfo.Width, dstInfo.Height, level); + int srcMipW = Math.Max(1, srcStorageInfo.Width >> (int)copySrcLevel); + int srcMipH = Math.Max(1, srcStorageInfo.Height >> (int)copySrcLevel); + int dstMipW = Math.Max(1, dstStorageInfo.Width >> (int)copyDstLevel); + int dstMipH = Math.Max(1, dstStorageInfo.Height >> (int)copyDstLevel); + + (srcOffsets.Element0, srcOffsets.Element1) = ClampOffsetsToMip(srcOffsets.Element0, srcOffsets.Element1, srcMipW, srcMipH); + (dstOffsets.Element0, dstOffsets.Element1) = ClampOffsetsToMip(dstOffsets.Element0, dstOffsets.Element1, dstMipW, dstMipH); + ImageBlit region = new() { SrcSubresource = srcSl, @@ -121,6 +138,8 @@ namespace Ryujinx.Graphics.Vulkan Image dstImage, TextureCreateInfo srcInfo, TextureCreateInfo dstInfo, + TextureCreateInfo srcStorageInfo, + TextureCreateInfo dstStorageInfo, int srcViewLayer, int dstViewLayer, int srcViewLevel, @@ -151,6 +170,8 @@ namespace Ryujinx.Graphics.Vulkan dstImage, srcInfo, dstInfo, + srcStorageInfo, + dstStorageInfo, srcViewLayer, dstViewLayer, srcViewLevel, @@ -186,6 +207,8 @@ namespace Ryujinx.Graphics.Vulkan Image dstImage, TextureCreateInfo srcInfo, TextureCreateInfo dstInfo, + TextureCreateInfo srcStorageInfo, + TextureCreateInfo dstStorageInfo, int srcViewLayer, int dstViewLayer, int srcViewLevel, @@ -314,6 +337,14 @@ namespace Ryujinx.Graphics.Vulkan int copyWidth = sizeInBlocks ? BitUtils.DivRoundUp(width, blockWidth) : width; int copyHeight = sizeInBlocks ? BitUtils.DivRoundUp(height, blockHeight) : height; + int srcMipW = Math.Max(1, srcStorageInfo.Width >> (srcViewLevel + srcLevel + level)); + int srcMipH = Math.Max(1, srcStorageInfo.Height >> (srcViewLevel + srcLevel + level)); + int dstMipW = Math.Max(1, dstStorageInfo.Width >> (dstViewLevel + dstLevel + level)); + int dstMipH = Math.Max(1, dstStorageInfo.Height >> (dstViewLevel + dstLevel + level)); + + copyWidth = Math.Min(copyWidth, Math.Min(srcMipW, dstMipW)); + copyHeight = Math.Min(copyHeight, Math.Min(srcMipH, dstMipH)); + Extent3D extent = new((uint)copyWidth, (uint)copyHeight, (uint)srcDepth); if (srcInfo.Samples > 1 && srcInfo.Samples != dstInfo.Samples) diff --git a/src/Ryujinx.Graphics.Vulkan/TextureStorage.cs b/src/Ryujinx.Graphics.Vulkan/TextureStorage.cs index 46cd5b4be..b102efaf2 100644 --- a/src/Ryujinx.Graphics.Vulkan/TextureStorage.cs +++ b/src/Ryujinx.Graphics.Vulkan/TextureStorage.cs @@ -67,6 +67,8 @@ namespace Ryujinx.Graphics.Vulkan public VkFormat VkFormat { get; } + public ImageUsageFlags UsageFlags { get; } + public unsafe TextureStorage( VulkanRenderer gd, Device device, @@ -93,7 +95,8 @@ namespace Ryujinx.Graphics.Vulkan SampleCountFlags sampleCountFlags = ConvertToSampleCountFlags(gd.Capabilities.SupportedSampleCounts, (uint)info.Samples); - ImageUsageFlags usage = GetImageUsage(info.Format, gd.Capabilities, isMsImageStorageSupported, true); + ImageUsageFlags usage = GetImageUsage(info.Format, gd.Capabilities, isMsImageStorageSupported); + UsageFlags = usage; ImageCreateFlags flags = ImageCreateFlags.CreateMutableFormatBit | ImageCreateFlags.CreateExtendedUsageBit; @@ -159,7 +162,7 @@ namespace Ryujinx.Graphics.Vulkan _imageAuto = new Auto(new DisposableImage(_gd.Api, device, _image)); - InitialTransition(ImageLayout.Preinitialized, ImageLayout.General); + InitialTransition(ImageLayout.Undefined, ImageLayout.General); } _slices = new TextureSliceInfo[levels * _depthOrLayers]; @@ -307,7 +310,7 @@ namespace Ryujinx.Graphics.Vulkan } } - public static ImageUsageFlags GetImageUsage(Format format, in HardwareCapabilities capabilities, bool isMsImageStorageSupported, bool extendedUsage) + public static ImageUsageFlags GetImageUsage(Format format, in HardwareCapabilities capabilities, bool isMsImageStorageSupported) { ImageUsageFlags usage = DefaultUsageFlags; @@ -320,7 +323,7 @@ namespace Ryujinx.Graphics.Vulkan usage |= ImageUsageFlags.ColorAttachmentBit; } - if ((format.IsImageCompatible && isMsImageStorageSupported) || extendedUsage) + if (format.IsImageCompatible && isMsImageStorageSupported) { usage |= ImageUsageFlags.StorageBit; } diff --git a/src/Ryujinx.Graphics.Vulkan/TextureView.cs b/src/Ryujinx.Graphics.Vulkan/TextureView.cs index 4513c804f..b6c0b8369 100644 --- a/src/Ryujinx.Graphics.Vulkan/TextureView.cs +++ b/src/Ryujinx.Graphics.Vulkan/TextureView.cs @@ -64,7 +64,7 @@ namespace Ryujinx.Graphics.Vulkan bool isMsImageStorageSupported = gd.Capabilities.SupportsShaderStorageImageMultisample || !info.Target.IsMultisample; VkFormat format = _gd.FormatCapabilities.ConvertToVkFormat(info.Format, isMsImageStorageSupported); - ImageUsageFlags usage = TextureStorage.GetImageUsage(info.Format, gd.Capabilities, isMsImageStorageSupported, false); + ImageUsageFlags usage = TextureStorage.GetImageUsage(info.Format, gd.Capabilities, isMsImageStorageSupported) & storage.UsageFlags; uint levels = (uint)info.Levels; uint layers = (uint)info.GetLayers(); @@ -133,6 +133,8 @@ namespace Ryujinx.Graphics.Vulkan shaderUsage |= ImageUsageFlags.StorageBit; } + shaderUsage &= storage.UsageFlags; + _imageView = CreateImageView(componentMapping, subresourceRange, type, shaderUsage); // Framebuffer attachments and storage images requires a identity component mapping. @@ -257,6 +259,8 @@ namespace Ryujinx.Graphics.Vulkan dstImage, src.Info, dst.Info, + src.Storage.Info, + dst.Storage.Info, src.FirstLayer, dst.FirstLayer, src.FirstLevel, @@ -310,6 +314,8 @@ namespace Ryujinx.Graphics.Vulkan dstImage, src.Info, dst.Info, + src.Storage.Info, + dst.Storage.Info, src.FirstLayer, dst.FirstLayer, src.FirstLevel, @@ -385,6 +391,8 @@ namespace Ryujinx.Graphics.Vulkan dst.GetImage().Get(cbs).Value, src.Info, dst.Info, + src.Storage.Info, + dst.Storage.Info, src.FirstLayer, dst.FirstLayer, src.FirstLevel, @@ -410,6 +418,8 @@ namespace Ryujinx.Graphics.Vulkan dst.GetImage().Get(cbs).Value, src.Info, dst.Info, + src.Storage.Info, + dst.Storage.Info, srcRegion, dstRegion, src.FirstLayer, @@ -463,6 +473,8 @@ namespace Ryujinx.Graphics.Vulkan dstImage.Get(cbs).Value, src.Info, dst.Info, + src.Storage.Info, + dst.Storage.Info, srcRegion, dstRegion, src.FirstLayer, diff --git a/src/Ryujinx.Graphics.Vulkan/VertexBufferState.cs b/src/Ryujinx.Graphics.Vulkan/VertexBufferState.cs index 236ab8721..da4edaa6a 100644 --- a/src/Ryujinx.Graphics.Vulkan/VertexBufferState.cs +++ b/src/Ryujinx.Graphics.Vulkan/VertexBufferState.cs @@ -68,9 +68,7 @@ namespace Ryujinx.Graphics.Vulkan int stride = (_stride + (alignment - 1)) & -alignment; int newSize = (_size / _stride) * stride; - Buffer buffer = autoBuffer.Get(cbs, 0, newSize).Value; - - updater.BindVertexBuffer(cbs, binding, buffer, 0, (ulong)newSize, (ulong)stride); + updater.BindVertexBuffer(cbs, binding, autoBuffer, 0, newSize, (ulong)stride); _buffer = autoBuffer; @@ -93,11 +91,7 @@ namespace Ryujinx.Graphics.Vulkan if (autoBuffer != null) { - int offset = _offset; - bool mirrorable = _size <= VertexBufferMaxMirrorable; - Buffer buffer = mirrorable ? autoBuffer.GetMirrorable(cbs, ref offset, _size, out _).Value : autoBuffer.Get(cbs, offset, _size).Value; - - updater.BindVertexBuffer(cbs, binding, buffer, (ulong)offset, (ulong)_size, (ulong)_stride); + updater.BindVertexBuffer(cbs, binding, autoBuffer, _offset, _size, (ulong)_stride); } } diff --git a/src/Ryujinx.Graphics.Vulkan/VertexBufferUpdater.cs b/src/Ryujinx.Graphics.Vulkan/VertexBufferUpdater.cs index 94269dd76..c2c2ba6f2 100644 --- a/src/Ryujinx.Graphics.Vulkan/VertexBufferUpdater.cs +++ b/src/Ryujinx.Graphics.Vulkan/VertexBufferUpdater.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using VkBuffer = Silk.NET.Vulkan.Buffer; namespace Ryujinx.Graphics.Vulkan @@ -15,6 +16,10 @@ namespace Ryujinx.Graphics.Vulkan private readonly NativeArray _sizes; private readonly NativeArray _strides; + private readonly Auto[] _bufferAutos; + private readonly int[] _bufferOffsetsForGet; + private readonly int[] _bufferSizesForGet; + public VertexBufferUpdater(VulkanRenderer gd) { _gd = gd; @@ -23,9 +28,13 @@ namespace Ryujinx.Graphics.Vulkan _offsets = new NativeArray(Constants.MaxVertexBuffers); _sizes = new NativeArray(Constants.MaxVertexBuffers); _strides = new NativeArray(Constants.MaxVertexBuffers); + + _bufferAutos = new Auto[Constants.MaxVertexBuffers]; + _bufferOffsetsForGet = new int[Constants.MaxVertexBuffers]; + _bufferSizesForGet = new int[Constants.MaxVertexBuffers]; } - public void BindVertexBuffer(CommandBufferScoped cbs, uint binding, VkBuffer buffer, ulong offset, ulong size, ulong stride) + public void BindVertexBuffer(CommandBufferScoped cbs, uint binding, Auto autoBuffer, int offset, int size, ulong stride) { if (_count == 0) { @@ -39,9 +48,11 @@ namespace Ryujinx.Graphics.Vulkan int index = (int)_count; - _buffers[index] = buffer; - _offsets[index] = offset; - _sizes[index] = size; + _bufferAutos[index] = autoBuffer; + _bufferOffsetsForGet[index] = offset; + _bufferSizesForGet[index] = size; + _offsets[index] = (ulong)offset; + _sizes[index] = (ulong)size; _strides[index] = stride; _count++; @@ -51,23 +62,65 @@ namespace Ryujinx.Graphics.Vulkan { if (_count != 0) { - if (_gd.Capabilities.SupportsExtendedDynamicState) + int count = (int)_count; + uint baseBinding = _baseBinding; + _count = 0; + + Auto[] autos = ArrayPool>.Shared.Rent(count); + Span getOffsets = stackalloc int[Constants.MaxVertexBuffers]; + Span getSizes = stackalloc int[Constants.MaxVertexBuffers]; + Span offsets = stackalloc ulong[Constants.MaxVertexBuffers]; + Span sizes = stackalloc ulong[Constants.MaxVertexBuffers]; + Span strides = stackalloc ulong[Constants.MaxVertexBuffers]; + Span buffers = stackalloc VkBuffer[Constants.MaxVertexBuffers]; + + for (int i = 0; i < count; i++) { - _gd.ExtendedDynamicStateApi.CmdBindVertexBuffers2( - cbs.CommandBuffer, - _baseBinding, - _count, - _buffers.Pointer, - _offsets.Pointer, - _sizes.Pointer, - _strides.Pointer); - } - else - { - _gd.Api.CmdBindVertexBuffers(cbs.CommandBuffer, _baseBinding, _count, _buffers.Pointer, _offsets.Pointer); + autos[i] = _bufferAutos[i]; + _bufferAutos[i] = null; + getOffsets[i] = _bufferOffsetsForGet[i]; + getSizes[i] = _bufferSizesForGet[i]; + offsets[i] = _offsets[i]; + sizes[i] = _sizes[i]; + strides[i] = _strides[i]; } - _count = 0; + try + { + for (int i = 0; i < count; i++) + { + buffers[i] = autos[i].Get(cbs, getOffsets[i], getSizes[i]).Value; + autos[i] = null; + } + + for (int i = 0; i < count; i++) + { + _buffers[i] = buffers[i]; + _offsets[i] = offsets[i]; + _sizes[i] = sizes[i]; + _strides[i] = strides[i]; + } + + if (_gd.Capabilities.SupportsExtendedDynamicState) + { + _gd.ExtendedDynamicStateApi.CmdBindVertexBuffers2( + cbs.CommandBuffer, + baseBinding, + (uint)count, + _buffers.Pointer, + _offsets.Pointer, + _sizes.Pointer, + _strides.Pointer); + } + else + { + _gd.Api.CmdBindVertexBuffers(cbs.CommandBuffer, baseBinding, (uint)count, _buffers.Pointer, _offsets.Pointer); + } + } + finally + { + ArrayPool>.Shared.Return(autos, clearArray: true); + } } } diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs b/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs index 02c4e6873..9cd8f90d7 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs @@ -494,6 +494,8 @@ namespace Ryujinx.Graphics.Vulkan UniformBufferStandardLayout = supportedPhysicalDeviceVulkan12Features.UniformBufferStandardLayout, UniformAndStorageBuffer8BitAccess = supportedPhysicalDeviceVulkan12Features.UniformAndStorageBuffer8BitAccess, StorageBuffer8BitAccess = supportedPhysicalDeviceVulkan12Features.StorageBuffer8BitAccess, + ShaderSampledImageArrayNonUniformIndexing = supportedPhysicalDeviceVulkan12Features.ShaderSampledImageArrayNonUniformIndexing, + ShaderStorageImageArrayNonUniformIndexing = supportedPhysicalDeviceVulkan12Features.ShaderStorageImageArrayNonUniformIndexing, }; pExtendedFeatures = &featuresVk12; diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs index 5f1c50b00..a0b764158 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs @@ -435,8 +435,8 @@ namespace Ryujinx.Graphics.Vulkan _physicalDevice.IsDeviceExtensionPresent(ExtExtendedDynamicState.ExtensionName), features2.Features.MultiViewport && !(IsMoltenVk && Vendor == Vendor.Amd), // Workaround for AMD on MoltenVK issue featuresRobustness2.NullDescriptor || IsMoltenVk, - supportsPushDescriptors && !IsMoltenVk, - propertiesPushDescriptor.MaxPushDescriptors, + supportsPushDescriptors, + IsMoltenVk ? 16 : propertiesPushDescriptor.MaxPushDescriptors, // In case an old version of MoltenVK is used, apply a limit to prevent vertex explosions. featuresPrimitiveTopologyListRestart.PrimitiveTopologyListRestart, featuresPrimitiveTopologyListRestart.PrimitiveTopologyPatchListRestart, supportsTransformFeedback, @@ -775,7 +775,11 @@ namespace Ryujinx.Graphics.Vulkan supportsShaderBallot: false, supportsShaderBarrierDivergence: Vendor != Vendor.Intel, supportsShaderFloat64: Capabilities.SupportsShaderFloat64, - supportsTextureGatherOffsets: features2.Features.ShaderImageGatherExtended && !IsMoltenVk, + + supportsShaderNonUniformIndexing: + featuresVk12.ShaderSampledImageArrayNonUniformIndexing && + featuresVk12.ShaderStorageImageArrayNonUniformIndexing, + supportsTextureGatherOffsets: features2.Features.ShaderImageGatherExtended, supportsTextureShadowLod: false, supportsVertexStoreAndAtomics: features2.Features.VertexPipelineStoresAndAtomics, supportsViewportIndexVertexTessellation: featuresVk12.ShaderOutputViewportIndex, diff --git a/src/Ryujinx.Graphics.Vulkan/Window.cs b/src/Ryujinx.Graphics.Vulkan/Window.cs index 0a0d970c1..eb8d433be 100644 --- a/src/Ryujinx.Graphics.Vulkan/Window.cs +++ b/src/Ryujinx.Graphics.Vulkan/Window.cs @@ -391,12 +391,12 @@ namespace Ryujinx.Graphics.Vulkan { if (_effect != null) { + _gd.FlushAllCommands(); _gd.CommandBufferPool.Return( cbs, null, [PipelineStageFlags.ColorAttachmentOutputBit], null); - _gd.FlushAllCommands(); cbs.GetFence().Wait(); cbs = _gd.CommandBufferPool.Rent(); } @@ -455,6 +455,8 @@ namespace Ryujinx.Graphics.Vulkan ImageLayout.General, ImageLayout.PresentSrcKhr); + _gd.FlushAllCommands(); + _gd.CommandBufferPool.Return( cbs, [_imageAvailableSemaphores[semaphoreIndex]], diff --git a/src/Ryujinx.HLE.Generators/Ryujinx.HLE.Generators.csproj b/src/Ryujinx.HLE.Generators/Ryujinx.HLE.Generators.csproj index 4791a3b27..b5335282e 100644 --- a/src/Ryujinx.HLE.Generators/Ryujinx.HLE.Generators.csproj +++ b/src/Ryujinx.HLE.Generators/Ryujinx.HLE.Generators.csproj @@ -14,7 +14,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + + + all + diff --git a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/LibraryAppletProxy/ILibraryAppletSelfAccessor.cs b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/LibraryAppletProxy/ILibraryAppletSelfAccessor.cs index fc02ea172..fa986de93 100644 --- a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/LibraryAppletProxy/ILibraryAppletSelfAccessor.cs +++ b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/LibraryAppletProxy/ILibraryAppletSelfAccessor.cs @@ -1,4 +1,5 @@ using Ryujinx.Common; +using Ryujinx.Common.Logging; using System; namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.LibraryAppletProxy @@ -44,6 +45,35 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib return ResultCode.Success; } + + [CommandCmif(1)] + // PushOutData(object) + public ResultCode PushOutData(ServiceCtx context) + { + IStorage appletData = GetObject(context, 0); + + if (appletData == null || appletData.Data.Length == 0) // is this necessary? + { + return ResultCode.NullObject; + } + + _appletStandalone.InputData.Enqueue(appletData.Data); + + return ResultCode.Success; + } + + [CommandCmif(10)] + // ExitProcessAndReturn -> nn::am::service::LibraryAppletInfo + public ResultCode ExitProcessAndReturn(ServiceCtx context) + { + // Exits the LibraryApplet and returns to running the title which launched this LibraryApplet (qlaunch for example). + // On success, official sw will enter an infinite loop with sleep-thread value 86400000000000. + // Since we don't currently support qlaunch, it's fine to stub it. + + Logger.Stub?.PrintStub(LogClass.Service); + return ResultCode.Success; + } + [CommandCmif(11)] // GetLibraryAppletInfo() -> nn::am::service::LibraryAppletInfo @@ -67,7 +97,8 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib AppletIdentifyInfo appletIdentifyInfo = new() { AppletId = AppletId.QLaunch, - TitleId = 0x0100000000001000, + // 0x4 padding + TitleId = 0x0100000000001000, // qlaunch systemAppletMenu title ID }; context.ResponseData.WriteStruct(appletIdentifyInfo); diff --git a/src/Ryujinx.HLE/HOS/Services/Caps/CaptureManager.cs b/src/Ryujinx.HLE/HOS/Services/Caps/CaptureManager.cs index f47c663ed..651f92986 100644 --- a/src/Ryujinx.HLE/HOS/Services/Caps/CaptureManager.cs +++ b/src/Ryujinx.HLE/HOS/Services/Caps/CaptureManager.cs @@ -1,7 +1,9 @@ +using Ryujinx.Common.Logging; using Ryujinx.Common.Memory; using Ryujinx.HLE.HOS.Services.Caps.Types; using SkiaSharp; using System; +using System.Globalization; using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -9,16 +11,20 @@ using System.Security.Cryptography; namespace Ryujinx.HLE.HOS.Services.Caps { - class CaptureManager + internal class CaptureManager { - private readonly string _sdCardPath; + public CaptureManager(Switch device) + { + _ = device; + } + private readonly string _sdCardPath = FileSystem.VirtualFileSystem.GetSdCardPath(); private uint _shimLibraryVersion; - public CaptureManager(Switch device) - { - _sdCardPath = FileSystem.VirtualFileSystem.GetSdCardPath(); - } + private const int ScreenshotWidth = 1280; + private const int ScreenshotHeight = 720; + private const int ScreenshotBytesPerPixel = 4; + private const int ScreenshotDataSize = ScreenshotWidth * ScreenshotHeight * ScreenshotBytesPerPixel; // 0x384000 public ResultCode SetShimLibraryVersion(ServiceCtx context) { @@ -53,84 +59,94 @@ namespace Ryujinx.HLE.HOS.Services.Caps return resultCode; } - public ResultCode SaveScreenShot(byte[] screenshotData, ulong appletResourceUserId, ulong titleId, out ApplicationAlbumEntry applicationAlbumEntry) + public ResultCode SaveScreenShot( + byte[] screenshotData, + ulong appletResourceUserId, + ulong titleId, + out ApplicationAlbumEntry applicationAlbumEntry) { + Logger.Stub?.PrintStub(LogClass.ServiceCaps, new + { + appletResourceUserId, + titleId, + screenshotDataLength = screenshotData?.Length ?? 0, + }); + applicationAlbumEntry = default; - if (screenshotData.Length == 0) + if (screenshotData == null || screenshotData.Length == 0) { return ResultCode.NullInputBuffer; } - /* - // NOTE: On our current implementation, appletResourceUserId starts at 0, disable it for now. - if (appletResourceUserId == 0) + if (screenshotData.Length < ScreenshotDataSize) + { + Logger.Warning?.PrintMsg( + LogClass.ServiceCaps, + $"Invalid screenshot buffer size 0x{screenshotData.Length:X}; expected at least 0x{ScreenshotDataSize:X}."); + + return ResultCode.NullInputBuffer; + } + + DateTime currentDateTime = DateTime.Now; + + applicationAlbumEntry = new ApplicationAlbumEntry() + { + Size = (ulong)Unsafe.SizeOf(), + TitleId = titleId, + AlbumFileDateTime = new AlbumFileDateTime() + { + Year = (ushort)currentDateTime.Year, + Month = (byte)currentDateTime.Month, + Day = (byte)currentDateTime.Day, + Hour = (byte)currentDateTime.Hour, + Minute = (byte)currentDateTime.Minute, + Second = (byte)currentDateTime.Second, + UniqueId = 0, + }, + AlbumStorage = AlbumStorage.Sd, + ContentType = ContentType.Screenshot, + Padding = new Array5(), + Unknown0x1f = 1, + }; + + // NOTE: The hex hash is a HMAC-SHA256 (first 32 bytes) using a hardcoded secret key over the titleId, we can simulate it by hashing the titleId instead. + string hash = Convert.ToHexString(SHA256.HashData(BitConverter.GetBytes(titleId)))[..0x20]; + + string folderPath = Path.Combine( + _sdCardPath, + "Nintendo", + "Album", + currentDateTime.Year.ToString("0000", CultureInfo.InvariantCulture), + currentDateTime.Month.ToString("00", CultureInfo.InvariantCulture), + currentDateTime.Day.ToString("00", CultureInfo.InvariantCulture)); + + string filePath = GenerateFilePath(folderPath, applicationAlbumEntry, currentDateTime, hash); + + _ = Directory.CreateDirectory(folderPath); + + while (File.Exists(filePath)) + { + applicationAlbumEntry.AlbumFileDateTime.UniqueId++; + filePath = GenerateFilePath(folderPath, applicationAlbumEntry, currentDateTime, hash); + } + + using SKBitmap bitmap = new(new SKImageInfo(ScreenshotWidth, ScreenshotHeight, SKColorType.Rgba8888)); + + IntPtr pixels = bitmap.GetPixels(); + + if (pixels == IntPtr.Zero) { return ResultCode.InvalidArgument; } - */ - /* - // Doesn't occur in our case. - if (applicationAlbumEntry == null) - { - return ResultCode.NullOutputBuffer; - } - */ + Marshal.Copy(screenshotData, 0, pixels, ScreenshotDataSize); - if (screenshotData.Length >= 0x384000) - { - DateTime currentDateTime = DateTime.Now; + using SKData data = bitmap.Encode(SKEncodedImageFormat.Jpeg, 80); + using FileStream file = File.OpenWrite(filePath); + data.SaveTo(file); - applicationAlbumEntry = new ApplicationAlbumEntry() - { - Size = (ulong)Unsafe.SizeOf(), - TitleId = titleId, - AlbumFileDateTime = new AlbumFileDateTime() - { - Year = (ushort)currentDateTime.Year, - Month = (byte)currentDateTime.Month, - Day = (byte)currentDateTime.Day, - Hour = (byte)currentDateTime.Hour, - Minute = (byte)currentDateTime.Minute, - Second = (byte)currentDateTime.Second, - UniqueId = 0, - }, - AlbumStorage = AlbumStorage.Sd, - ContentType = ContentType.Screenshot, - Padding = new Array5(), - Unknown0x1f = 1, - }; - - // NOTE: The hex hash is a HMAC-SHA256 (first 32 bytes) using a hardcoded secret key over the titleId, we can simulate it by hashing the titleId instead. - string hash = Convert.ToHexString(SHA256.HashData(BitConverter.GetBytes(titleId)))[..0x20]; - string folderPath = Path.Combine(_sdCardPath, "Nintendo", "Album", currentDateTime.Year.ToString("00"), currentDateTime.Month.ToString("00"), currentDateTime.Day.ToString("00")); - string filePath = GenerateFilePath(folderPath, applicationAlbumEntry, currentDateTime, hash); - - // TODO: Handle that using the FS service implementation and return the right error code instead of throwing exceptions. - Directory.CreateDirectory(folderPath); - - while (File.Exists(filePath)) - { - applicationAlbumEntry.AlbumFileDateTime.UniqueId++; - - filePath = GenerateFilePath(folderPath, applicationAlbumEntry, currentDateTime, hash); - } - - // NOTE: The saved JPEG file doesn't have the limitation in the extra EXIF data. - using SKBitmap bitmap = new(new SKImageInfo(1280, 720, SKColorType.Rgba8888, SKAlphaType.Premul)); - int dataLen = screenshotData.Length > bitmap.ByteCount ? bitmap.ByteCount : screenshotData.Length; - - Marshal.Copy(screenshotData, 0, bitmap.GetPixels(), dataLen); - - using SKData data = bitmap.Encode(SKEncodedImageFormat.Jpeg, 80); - using FileStream file = File.OpenWrite(filePath); - data.SaveTo(file); - - return ResultCode.Success; - } - - return ResultCode.NullInputBuffer; + return ResultCode.Success; } private string GenerateFilePath(string folderPath, ApplicationAlbumEntry applicationAlbumEntry, DateTime currentDateTime, string hash) diff --git a/src/Ryujinx.HLE/HOS/Services/Caps/IScreenShotApplicationService.cs b/src/Ryujinx.HLE/HOS/Services/Caps/IScreenShotApplicationService.cs index 0723b57cc..2ccb7c598 100644 --- a/src/Ryujinx.HLE/HOS/Services/Caps/IScreenShotApplicationService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Caps/IScreenShotApplicationService.cs @@ -1,13 +1,19 @@ using Ryujinx.Common; +using Ryujinx.Common.Logging; using Ryujinx.HLE.HOS.Services.Caps.Types; namespace Ryujinx.HLE.HOS.Services.Caps { [Service("caps:su")] // 6.0.0+ - class IScreenShotApplicationService : IpcService + internal class IScreenShotApplicationService : IpcService { - public IScreenShotApplicationService(ServiceCtx context) { } + private const ulong ScreenshotDataSize = 0x384000; + private const ulong ApplicationDataSize = 0x404; + public IScreenShotApplicationService(ServiceCtx context) + { + _ = context; + } [CommandCmif(32)] // 7.0.0+ // SetShimLibraryVersion(pid, u64, nn::applet::AppletResourceUserId) public ResultCode SetShimLibraryVersion(ServiceCtx context) @@ -33,6 +39,15 @@ namespace Ryujinx.HLE.HOS.Services.Caps ulong screenshotDataPosition = context.Request.SendBuff[0].Position; ulong screenshotDataSize = context.Request.SendBuff[0].Size; + if (screenshotDataSize < ScreenshotDataSize) + { + Logger.Warning?.PrintMsg( + LogClass.ServiceCaps, + $"Invalid screenshot buffer size 0x{screenshotDataSize:X}; expected at least 0x{ScreenshotDataSize:X}."); + + return ResultCode.NullInputBuffer; + } + byte[] screenshotData = context.Memory.GetSpan(screenshotDataPosition, (int)screenshotDataSize, true).ToArray(); ResultCode resultCode = context.Device.System.CaptureManager.SaveScreenShot(screenshotData, appletResourceUserId, context.Device.Processes.ActiveApplication.ProgramId, out ApplicationAlbumEntry applicationAlbumEntry); @@ -60,6 +75,24 @@ namespace Ryujinx.HLE.HOS.Services.Caps ulong screenshotDataPosition = context.Request.SendBuff[1].Position; ulong screenshotDataSize = context.Request.SendBuff[1].Size; + if (applicationDataSize != ApplicationDataSize) + { + Logger.Warning?.PrintMsg( + LogClass.ServiceCaps, + $"Invalid ApplicationData size 0x{applicationDataSize:X}; expected 0x{ApplicationDataSize:X}."); + + return ResultCode.InvalidArgument; + } + + if (screenshotDataSize < ScreenshotDataSize) + { + Logger.Warning?.PrintMsg( + LogClass.ServiceCaps, + $"Invalid screenshot buffer size 0x{screenshotDataSize:X}; expected at least 0x{ScreenshotDataSize:X}."); + + return ResultCode.NullInputBuffer; + } + // TODO: Parse the application data: At 0x00 it's UserData (Size of 0x400), at 0x404 it's a uint UserDataSize (Always empty for now). _ = context.Memory.GetSpan(applicationDataPosition, (int)applicationDataSize).ToArray(); @@ -88,6 +121,23 @@ namespace Ryujinx.HLE.HOS.Services.Caps ulong screenshotDataPosition = context.Request.SendBuff[1].Position; ulong screenshotDataSize = context.Request.SendBuff[1].Size; + if (userIdListSize != 0x88) + { + Logger.Warning?.PrintMsg( + LogClass.ServiceCaps, + $"Invalid UserIdList size 0x{userIdListSize:X}; expected 0x88."); + return ResultCode.InvalidArgument; + } + + if (screenshotDataSize < ScreenshotDataSize) + { + Logger.Warning?.PrintMsg( + LogClass.ServiceCaps, + $"Invalid screenshot buffer size 0x{screenshotDataSize:X}; expected at least 0x{ScreenshotDataSize:X}."); + + return ResultCode.NullInputBuffer; + } + // TODO: Parse the UserIdList. _ = context.Memory.GetSpan(userIdListPosition, (int)userIdListSize).ToArray(); diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs index 71d1623f3..2f764e99f 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs @@ -5,7 +5,6 @@ using Ryujinx.Common.Logging; using Ryujinx.Common.Memory; using Ryujinx.Common.Utilities; using Ryujinx.Cpu; -using Ryujinx.HLE.Exceptions; using Ryujinx.HLE.HOS.Ipc; using Ryujinx.HLE.HOS.Kernel.Threading; using Ryujinx.HLE.HOS.Services.Ldn.Types; @@ -15,7 +14,6 @@ using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; using Ryujinx.Horizon.Common; using Ryujinx.Memory; using System; -using System.ComponentModel; using System.IO; using System.Net; using System.Net.NetworkInformation; @@ -68,10 +66,11 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (localCommunicationId == localCommunicationIdChecked) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,"CheckLocalCommumicationIdPermission: Checked!"); return true; } } - + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,"CheckLocalCommumicationIdPermission: Check failed!"); return false; } @@ -82,7 +81,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (_nifmResultCode != ResultCode.Success) { context.ResponseData.Write((int)NetworkState.Error); - + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,$"GetState: _nifmResultCode = {_nifmResultCode.ToString()}"); return ResultCode.Success; } @@ -114,12 +113,14 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,$"GetNetworkInfo: _nifmResultCode = {_nifmResultCode.ToString()}"); return _nifmResultCode; } ResultCode resultCode = GetNetworkInfoImpl(out NetworkInfo networkInfo); if (resultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,$"GetState: resultCode = {resultCode.ToString()}"); return resultCode; } @@ -135,18 +136,22 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (_state == NetworkState.StationConnected) { networkInfo = _station.NetworkInfo; + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,"GetNetworkInfoImpl: _station"); } else if (_state == NetworkState.AccessPointCreated) { networkInfo = _accessPoint.NetworkInfo; + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,"GetNetworkInfoImpl: _accessPoint"); } else { networkInfo = new NetworkInfo(); - + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,"GetNetworkInfoImpl: Invalid state!"); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,$"GetNetworkInfoImpl: networkInfo = {networkInfo}"); return ResultCode.InvalidState; } + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,$"GetNetworkInfoImpl: networkInfo = {networkInfo}"); return ResultCode.Success; } @@ -198,7 +203,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } else { - Logger.Info?.Print(LogClass.ServiceLdn, $"Console's LDN IP is \"{unicastAddress.Address}\"."); + Logger.NetLog?.Print(LogClass.ServiceLdn, $"Console's LDN IP is \"{unicastAddress.Address}\"."); context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.Address)); context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.IPv4Mask)); @@ -206,7 +211,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } else { - Logger.Info?.Print(LogClass.ServiceLdn, $"LDN obtained proxy IP."); + Logger.NetLog?.Print(LogClass.ServiceLdn, $"LDN obtained proxy IP."); context.ResponseData.Write(config.ProxyIp); context.ResponseData.Write(config.ProxySubnetMask); @@ -227,7 +232,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator // NOTE: Returns ResultCode.InvalidArgument if _disconnectReason is null, doesn't occur in our case. context.ResponseData.Write((short)_disconnectReason); - + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetDisconnectReason: {_disconnectReason}"); return ResultCode.Success; } @@ -247,12 +252,14 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetSecurityParameter: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } ResultCode resultCode = GetNetworkInfoImpl(out NetworkInfo networkInfo); if (resultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetSecurityParameter: resultCode = {resultCode}"); return resultCode; } @@ -263,7 +270,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator }; context.ResponseData.WriteStruct(securityParameter); - + + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetSecurityParameter: securityParameter = {securityParameter}"); return ResultCode.Success; } @@ -273,12 +281,14 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetNetworkConfig: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } ResultCode resultCode = GetNetworkInfoImpl(out NetworkInfo networkInfo); if (resultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetNetworkConfig: resultCode = {resultCode}"); return resultCode; } @@ -292,6 +302,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator }; context.ResponseData.WriteStruct(networkConfig); + + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetNetworkConfig: networkConfig = {networkConfig}"); return ResultCode.Success; } @@ -322,12 +334,14 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetNetworkInfoLatestUpdate: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } ResultCode resultCode = GetNetworkInfoImpl(out NetworkInfo networkInfo); if (resultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetNetworkInfoLatestUpdate: resultCode = {resultCode}"); return resultCode; } @@ -378,6 +392,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ScanImpl: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } @@ -400,6 +415,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (scanFilter.Ssid.Length <= 31) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ScanImpl: resultCode = {resultCode}"); return resultCode; } } @@ -408,11 +424,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (scanFilterFlag > ScanFilterFlag.All) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ScanImpl: resultCode = {resultCode}"); return resultCode; } if (_state - 3 >= NetworkState.AccessPoint) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "ScanImpl: Invalid state!"); resultCode = ResultCode.InvalidState; } else @@ -437,7 +455,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } } } - + + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ScanImpl: resultCode = {resultCode}"); return resultCode; } @@ -462,6 +481,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } } + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ScanInternal: availableGames = {availableGames}"); return ResultCode.Success; } @@ -502,7 +522,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator throw new ArgumentException($"{GetType().FullName}: Protocol value is not 1 or 3!! Protocol value: {protocolValue}"); } - Logger.Stub?.PrintStub(LogClass.ServiceLdn, $"Protocol value: {protocolValue}"); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"SetProtocol: protocolValue = {protocolValue}"); + Logger.Stub?.PrintStub(LogClass.ServiceLdn, new { protocolValue}); return ResultCode.Success; } @@ -512,11 +533,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"OpenAccessPoint: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } if (_state != NetworkState.Initialized) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "OpenAccessPoint: Invalid state!"); return ResultCode.InvalidState; } @@ -538,6 +561,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"CloseAccessPoint: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } @@ -547,6 +571,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } else { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "CloseAccessPoint: Invalid state!"); return ResultCode.InvalidState; } @@ -596,11 +621,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkConfig.IntentId.LocalCommunicationId); if (!isLocalCommunicationIdValid && NetworkClient.NeedsRealId) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "CreateNetworkImpl: Invalid object!"); return ResultCode.InvalidObject; } if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"CreateNetworkImpl: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } @@ -629,16 +656,22 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator AddressList addressList = MemoryMarshal.Cast(addressListBytes)[0]; _accessPoint.CreateNetworkPrivate(securityConfig, securityParameter, userConfig, networkConfig, addressList); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"CreateNetworkImpl: Created private network! " + + $"| securityConfig = {securityConfig} | securityParameter = {securityParameter} " + + $"| userConfig = {userConfig} | networkConfig = {networkConfig} | addressList = {addressList}"); } else { _accessPoint.CreateNetwork(securityConfig, userConfig, networkConfig); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"CreateNetworkImpl: Created network! " + + $"| securityConfig = {securityConfig} | userConfig = {userConfig} | networkConfig = {networkConfig}"); } return ResultCode.Success; } else { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "CreateNetworkImpl: Invalid state!"); return ResultCode.InvalidState; } } @@ -660,6 +693,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"DestroyNetworkImpl: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } @@ -676,9 +710,11 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator CloseAccessPoint(); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "DestroyNetworkImpl: Invalid state!"); return ResultCode.InvalidState; } + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "DestroyNetworkImpl: Invalid argument!"); return ResultCode.InvalidArgument; } @@ -695,14 +731,17 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"RejectImpl: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } if (_state != NetworkState.AccessPointCreated) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "RejectImpl: Invalid state!"); return ResultCode.InvalidState; // Must be network host to reject nodes. } + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"RejectImpl: disconnectReason = {disconnectReason} | nodeId = {nodeId}"); return NetworkClient.Reject(disconnectReason, nodeId); } @@ -714,11 +753,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"SetAdvertiseData: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } if (bufferSize is 0 or > LdnConst.AdvertiseDataSizeMax) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "SetAdvertiseData: Invalid argument!"); return ResultCode.InvalidArgument; } @@ -727,11 +768,12 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator byte[] advertiseData = new byte[bufferSize]; context.Memory.Read(bufferPosition, advertiseData); - + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"SetAdvertiseData: advertiseData = {advertiseData}"); return _accessPoint.SetAdvertiseData(advertiseData); } else { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "SetAdvertiseData: Invalid state!"); return ResultCode.InvalidState; } } @@ -744,20 +786,24 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"SetStationAcceptPolicy: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } if (acceptPolicy > AcceptPolicy.WhiteList) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "SetStationAcceptPolicy: Invalid argument!"); return ResultCode.InvalidArgument; } if (_state is NetworkState.AccessPoint or NetworkState.AccessPointCreated) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"SetStationAcceptPolicy: acceptPolicy = {acceptPolicy}"); return _accessPoint.SetStationAcceptPolicy(acceptPolicy); } else { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "SetStationAcceptPolicy: Invalid state!"); return ResultCode.InvalidState; } } @@ -768,6 +814,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"AddAcceptFilterEntry: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } @@ -782,6 +829,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ClearAcceptFilter: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } @@ -796,11 +844,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"OpenStation: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } if (_state != NetworkState.Initialized) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "OpenStation: Invalid state!"); return ResultCode.InvalidState; } @@ -813,6 +863,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator // NOTE: Calls nifm service and returns related result codes. // Since we use our own implementation we can return ResultCode.Success. + + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"OpenStation: _station = {_station}"); return ResultCode.Success; } @@ -823,6 +875,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"CloseStation: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } @@ -832,11 +885,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } else { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "CloseStation: Invalid state!"); return ResultCode.InvalidState; } SetState(NetworkState.Initialized); - + + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "CloseStation: Closed."); return ResultCode.Success; } @@ -901,11 +956,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkInfo.NetworkId.IntentId.LocalCommunicationId); if (!isLocalCommunicationIdValid && NetworkClient.NeedsRealId) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "ConnectImpl: Invalid object!"); return ResultCode.InvalidObject; } if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ConnectImpl: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } @@ -925,6 +982,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_state != NetworkState.Station) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "ConnectImpl: Invalid state!"); resultCode = ResultCode.InvalidState; } else @@ -932,10 +990,16 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (isPrivate) { resultCode = _station.ConnectPrivate(securityConfig, securityParameter, userConfig, localCommunicationVersion, optionUnknown, networkConfig); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ConnectImpl: Private connection established! " + + $"| securityConfig = {securityConfig} | securityParameter = {securityParameter} | userConfig = {userConfig} " + + $"| localCommunicationVersion = {localCommunicationVersion} | optionUnknown = {optionUnknown} | networkConfig = {networkConfig}"); } else { resultCode = _station.Connect(securityConfig, userConfig, localCommunicationVersion, optionUnknown, networkInfo); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ConnectImpl: Connection established! " + + $"| securityConfig = {securityConfig} | userConfig = {userConfig} " + + $"| localCommunicationVersion = {localCommunicationVersion} | optionUnknown = {optionUnknown} | networkConfig = {networkConfig}"); } } } @@ -943,6 +1007,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } } + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ConnectImpl: resultCode = {resultCode}"); + return resultCode; } @@ -957,6 +1023,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"DisconnectImpl: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } @@ -970,14 +1037,17 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator _disconnectReason = disconnectReason; + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"DisconnectImpl: _disconnectReason = {_disconnectReason}"); return ResultCode.Success; } CloseStation(); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "DisconnectImpl: Invalid state!"); return ResultCode.InvalidState; } + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "DisconnectImpl: Invalid argument!"); return ResultCode.InvalidArgument; } @@ -994,6 +1064,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"Finalize: _disconnectReason = {_disconnectReason}"); return _nifmResultCode; } @@ -1010,11 +1081,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator _stateChangeEventHandle = 0; } + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"Finalize: resultCode = {resultCode}"); return resultCode; } private ResultCode FinalizeImpl(bool isCausedBySystem) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "FinalizeImpl"); DisconnectReason disconnectReason; switch (_state) @@ -1138,7 +1211,6 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator NetworkClient.SetGameVersion(context.Device.Processes.ActiveApplication.ApplicationControlProperties.DisplayVersion); resultCode = ResultCode.Success; - _nifmResultCode = resultCode; SetState(NetworkState.Initialized); @@ -1152,6 +1224,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } } + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"InitializeImpl: resultCode = {resultCode}"); return resultCode; } diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs index f93b1c4cc..37fa722b7 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs @@ -132,7 +132,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu protected override void OnConnected() { - Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client connected a new session with Id {Id}"); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client connected a new session with Id {Id}"); UpdatePassphraseIfNeeded(); @@ -141,7 +141,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu protected override void OnDisconnected() { - Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client disconnected a session with Id {Id}"); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client disconnected a session with Id {Id}"); _passphrase = null; @@ -174,7 +174,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu protected override void OnError(SocketError error) { - Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client caught an error with code {error}"); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client caught an error with code {error}"); _error.Set(); } @@ -428,7 +428,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu } else { - Logger.Info?.Print(LogClass.ServiceLdn, $"Created a wireless P2P network on port {request.ExternalProxyPort}."); + Logger.NetLog?.Print(LogClass.ServiceLdn, $"Created a wireless P2P network on port {request.ExternalProxyPort}."); _hostedProxy.Start(); (_, UnicastIPAddressInformation unicastAddress) = NetworkHelpers.GetLocalInterface(); diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs index fa43f789e..b589c56a4 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common.Logging; using Ryujinx.Common.Memory; using Ryujinx.HLE.HOS.Services.Ldn.Types; using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; @@ -36,10 +37,12 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (Connected) { _parent.SetState(NetworkState.StationConnected); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,$"NetworkChanged: {NetworkInfo}"); } else { _parent.SetDisconnectReason(e.DisconnectReasonOrDefault(DisconnectReason.DestroyedByUser)); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,"NetworkChanged: Disconnected (DestroyedByUser)"); } } else diff --git a/src/Ryujinx.HLE/HOS/Services/Mii/DatabaseImpl.cs b/src/Ryujinx.HLE/HOS/Services/Mii/DatabaseImpl.cs index 4bb736992..cd2115132 100644 --- a/src/Ryujinx.HLE/HOS/Services/Mii/DatabaseImpl.cs +++ b/src/Ryujinx.HLE/HOS/Services/Mii/DatabaseImpl.cs @@ -81,8 +81,10 @@ namespace Ryujinx.HLE.HOS.Services.Mii return ResultCode.Success; } - public ResultCode UpdateLatest(DatabaseSessionMetadata metadata, IStoredData oldMiiData, SourceFlag flag, IStoredData newMiiData) where T : unmanaged + public ResultCode UpdateLatest(DatabaseSessionMetadata metadata, T oldMiiData, SourceFlag flag, out T newMiiData) where T : unmanaged, IStoredData { + newMiiData = default; + if (!flag.HasFlag(SourceFlag.Database)) { return ResultCode.NotFound; @@ -106,7 +108,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii newMiiData.SetFromStoreData(storeData); - if (oldMiiData == newMiiData) + if (oldMiiData.Equals(newMiiData)) { return ResultCode.NotUpdated; } @@ -286,6 +288,18 @@ namespace Ryujinx.HLE.HOS.Services.Mii return result; } + public ResultCode Append(DatabaseSessionMetadata metadata, CharInfo charInfo) + { + ResultCode result = _miiDatabase.Append(metadata, _utilityImpl, charInfo); + + if (result == ResultCode.Success) + { + result = _miiDatabase.SaveDatabase(); + } + + return result; + } + public ResultCode ConvertCharInfoToCoreData(CharInfo charInfo, out CoreData coreData) { coreData = new CoreData(); diff --git a/src/Ryujinx.HLE/HOS/Services/Mii/MiiDatabaseManager.cs b/src/Ryujinx.HLE/HOS/Services/Mii/MiiDatabaseManager.cs index 356d42a85..f5a173d8e 100644 --- a/src/Ryujinx.HLE/HOS/Services/Mii/MiiDatabaseManager.cs +++ b/src/Ryujinx.HLE/HOS/Services/Mii/MiiDatabaseManager.cs @@ -449,6 +449,32 @@ namespace Ryujinx.HLE.HOS.Services.Mii return ResultCode.Success; } + public ResultCode Append(DatabaseSessionMetadata metadata, UtilityImpl utilityImpl, CharInfo charInfo) + { + if (!charInfo.IsValid()) + { + return ResultCode.InvalidCharInfo; + } + + if (charInfo.Type == 1) + { + return ResultCode.InvalidOperationOnSpecialMii; + } + + CoreData coreData = new(); + coreData.SetFromCharInfo(charInfo); + + StoreData storeData; + + do + { + storeData = StoreData.BuildFromCoreData(utilityImpl, coreData); + } + while (_database.GetIndexByCreatorId(out _, storeData.CreateId)); + + return AddOrReplace(metadata, storeData); + } + public ResultCode Delete(DatabaseSessionMetadata metadata, CreateId createId) { if (!_database.GetIndexByCreatorId(out int index, createId)) diff --git a/src/Ryujinx.HLE/HOS/Services/Mii/StaticService/DatabaseServiceImpl.cs b/src/Ryujinx.HLE/HOS/Services/Mii/StaticService/DatabaseServiceImpl.cs index fc12e2533..c12517430 100644 --- a/src/Ryujinx.HLE/HOS/Services/Mii/StaticService/DatabaseServiceImpl.cs +++ b/src/Ryujinx.HLE/HOS/Services/Mii/StaticService/DatabaseServiceImpl.cs @@ -54,9 +54,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii.StaticService protected override ResultCode UpdateLatest(CharInfo oldCharInfo, SourceFlag flag, out CharInfo newCharInfo) { - newCharInfo = default; - - return _database.UpdateLatest(_metadata, oldCharInfo, flag, newCharInfo); + return _database.UpdateLatest(_metadata, oldCharInfo, flag, out newCharInfo); } protected override ResultCode BuildRandom(Age age, Gender gender, Race race, out CharInfo charInfo) @@ -113,14 +111,14 @@ namespace Ryujinx.HLE.HOS.Services.Mii.StaticService protected override ResultCode UpdateLatest1(StoreData oldStoreData, SourceFlag flag, out StoreData newStoreData) { - newStoreData = default; - if (!_isSystem) { + newStoreData = default; + return ResultCode.PermissionDenied; } - return _database.UpdateLatest(_metadata, oldStoreData, flag, newStoreData); + return _database.UpdateLatest(_metadata, oldStoreData, flag, out newStoreData); } protected override ResultCode FindIndex(CreateId createId, bool isSpecial, out int index) @@ -262,5 +260,10 @@ namespace Ryujinx.HLE.HOS.Services.Mii.StaticService { return _database.ConvertCharInfoToCoreData(charInfo, out coreData); } + + protected override ResultCode Append(CharInfo charInfo) + { + return _database.Append(_metadata, charInfo); + } } } diff --git a/src/Ryujinx.HLE/HOS/Services/Mii/StaticService/IDatabaseService.cs b/src/Ryujinx.HLE/HOS/Services/Mii/StaticService/IDatabaseService.cs index 1a1c20d6e..3f9fad4fb 100644 --- a/src/Ryujinx.HLE/HOS/Services/Mii/StaticService/IDatabaseService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Mii/StaticService/IDatabaseService.cs @@ -340,6 +340,15 @@ namespace Ryujinx.HLE.HOS.Services.Mii.StaticService return result; } + [CommandCmif(26)] // 10.2.0+ + // Append(nn::mii::CharInfo char_info) + public ResultCode Append(ServiceCtx context) + { + CharInfo charInfo = context.RequestData.ReadStruct(); + + return Append(charInfo); + } + private Span CreateByteSpanFromBuffer(ServiceCtx context, IpcBuffDesc ipcBuff, bool isOutput) { byte[] rawData; @@ -421,5 +430,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii.StaticService protected abstract ResultCode ConvertCoreDataToCharInfo(CoreData coreData, out CharInfo charInfo); protected abstract ResultCode ConvertCharInfoToCoreData(CharInfo charInfo, out CoreData coreData); + + protected abstract ResultCode Append(CharInfo charInfo); } } diff --git a/src/Ryujinx.HLE/HOS/Services/Notification/INotificationServices.cs b/src/Ryujinx.HLE/HOS/Services/Notification/INotificationServices.cs new file mode 100644 index 000000000..e44983fce --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Notification/INotificationServices.cs @@ -0,0 +1,24 @@ +namespace Ryujinx.HLE.HOS.Services.Notification +{ + [Service("notif:s")] // 9.0.0+ + class INotificationServices : IpcService + { + public INotificationServices(ServiceCtx context) { } + + [CommandCmif(1000)] // 9.0.0+ + // GetNotificationCount() -> nn::notification::server::INotificationSystemEventAccessor + public ResultCode GetNotificationCount(ServiceCtx context) + { + MakeObject(context, new INotificationSystemEventAccessor(context)); + return ResultCode.Success; + } + + [CommandCmif(1040)] // 9.0.0+ + // GetNotificationSendingNotifier() -> nn::notification::server::INotificationSystemEventAccessor + public ResultCode GetNotificationSendingNotifier(ServiceCtx context) + { + MakeObject(context, new INotificationSystemEventAccessor(context)); + return ResultCode.Success; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Notification/INotificationServicesForApplication.cs b/src/Ryujinx.HLE/HOS/Services/Notification/INotificationServicesForApplication.cs index 29f8bfa85..498fc52fb 100644 --- a/src/Ryujinx.HLE/HOS/Services/Notification/INotificationServicesForApplication.cs +++ b/src/Ryujinx.HLE/HOS/Services/Notification/INotificationServicesForApplication.cs @@ -1,8 +1,33 @@ +using Ryujinx.Common.Logging; + namespace Ryujinx.HLE.HOS.Services.Notification { [Service("notif:a")] // 9.0.0+ class INotificationServicesForApplication : IpcService { public INotificationServicesForApplication(ServiceCtx context) { } + + // Leaving this here since I can never find it: https://switchbrew.org/wiki/Glue_services + + [CommandCmif(520)] // 9.0.0+ + // ListAlarmSettings(nn::arp::ApplicationCertificate) -> s32 AlarmSettingsCount + public ResultCode ListAlarmSettings(ServiceCtx context) + { + // TO-DO: Currently just returns 0. Should read in an ApplicationCertificate. + int alarmSettingsCount = 0; + context.ResponseData.Write(alarmSettingsCount); + return ResultCode.Success; + } + + [CommandCmif(1000)] // 9.0.0+ + // Initialize(PID-descriptor, u64 pid_reserved) + public ResultCode Intialize(ServiceCtx context) + { + ulong pid = context.Request.HandleDesc.PId; + context.RequestData.ReadUInt64(); // pid placeholder, zero + + Logger.Stub?.PrintStub(LogClass.ServiceNotification, new { pid }); + return ResultCode.Success; + } } } diff --git a/src/Ryujinx.HLE/HOS/Services/Notification/INotificationServicesForSystem.cs b/src/Ryujinx.HLE/HOS/Services/Notification/INotificationServicesForSystem.cs deleted file mode 100644 index c5946be84..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Notification/INotificationServicesForSystem.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Notification -{ - [Service("notif:s")] // 9.0.0+ - class INotificationServicesForSystem : IpcService - { - public INotificationServicesForSystem(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Notification/INotificationSystemEventAccessor.cs b/src/Ryujinx.HLE/HOS/Services/Notification/INotificationSystemEventAccessor.cs new file mode 100644 index 000000000..8cca7cc6e --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Notification/INotificationSystemEventAccessor.cs @@ -0,0 +1,32 @@ +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Ipc; +using Ryujinx.HLE.HOS.Kernel.Threading; +using Ryujinx.Horizon.Common; +using System; + +namespace Ryujinx.HLE.HOS.Services.Notification +{ + class INotificationSystemEventAccessor : IpcService + { + + private readonly KEvent _getNotificationSendingNotifierEvent; + private int _getNotificationSendingNotifierEventHandle; + public INotificationSystemEventAccessor(ServiceCtx context) { } + + [CommandCmif(0)] // 9.0.0+ + // GetNotificationSendingNotifier() -> nn::notification::server::INotificationSystemEventAccessor + public ResultCode GetSystemEvent(ServiceCtx context) + { + if (_getNotificationSendingNotifierEventHandle == 0) + { + if (context.Process.HandleTable.GenerateHandle(_getNotificationSendingNotifierEvent.ReadableEvent, out _getNotificationSendingNotifierEventHandle) != Result.Success) + { + throw new InvalidOperationException("Out of handles!"); + } + } + + context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_getNotificationSendingNotifierEventHandle); + return ResultCode.Success; + } + } +} diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs index 48b5b724c..f217ecd0b 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs @@ -14,6 +14,7 @@ using Ryujinx.HLE.Loaders.Executables; using Ryujinx.HLE.Loaders.Processes.Extensions; using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.IO; using Path = System.IO.Path; @@ -27,17 +28,64 @@ namespace Ryujinx.HLE.Loaders.Processes private ulong _latestPid; - public ProcessResult ActiveApplication + private readonly object _pidLock = new(); + +#nullable enable + public ProcessResult? ActiveApplication { get { - if (!_processesByPid.TryGetValue(_latestPid, out ProcessResult value)) - throw new RyujinxException( - $"The HLE Process map did not have a process with ID {_latestPid}. Are you missing firmware?"); + lock (_pidLock) + { + // Check if _latestPid is still valid + if (_latestPid == 0) + { + return null; + } - return value; + // Verify process still exists in kernel (authoritative source) + if (!_device.System.KernelContext.Processes.TryGetValue(_latestPid, out HOS.Kernel.Process.KProcess? kernelProcess)) + { + // Process no longer exists in kernel, clear stale state + Logger.Warning?.Print(LogClass.Loader, + $"ActiveApplication PID {_latestPid} no longer exists in kernel, clearing stale state"); + + _processesByPid.TryRemove(_latestPid, out _); + _latestPid = 0; + TitleIDs.CurrentApplication.Value = null; + + return null; + } + + // Verify process still exists in ProcessLoader's dictionary + if (_processesByPid.TryGetValue(_latestPid, out ProcessResult? processResult)) + { + // Additional check: verify process state + if (kernelProcess.State == HOS.Kernel.Process.ProcessState.Exited || + kernelProcess.State == HOS.Kernel.Process.ProcessState.Exiting) + { + Logger.Warning?.Print(LogClass.Loader, + $"ActiveApplication PID {_latestPid} is in state {kernelProcess.State}, clearing"); + + _processesByPid.TryRemove(_latestPid, out _); + _latestPid = 0; + TitleIDs.CurrentApplication.Value = null; + + return null; + } + + return processResult; + } + + // Fallback: clear stale PID if not in our dictionary + Logger.Warning?.Print(LogClass.Loader, + $"ActiveApplication PID {_latestPid} not in ProcessLoader dictionary, clearing"); + _latestPid = 0; + return null; + } } } +#nullable disable public ProcessLoader(Switch device) { @@ -144,7 +192,7 @@ namespace Ryujinx.HLE.Loaders.Processes public bool LoadUnpackedNca(string exeFsDirPath, string romFsPath = null) { ProcessResult processResult = new LocalFileSystem(exeFsDirPath).Load(_device, romFsPath); - + if (processResult.ProcessId != 0 && _processesByPid.TryAdd(processResult.ProcessId, processResult)) { if (processResult.Start(_device)) @@ -277,5 +325,39 @@ namespace Ryujinx.HLE.Loaders.Processes return false; } + + /// + /// Clears a specific process from the ProcessLoader's tracking. + /// This should be called when a process exits or is terminated. + /// + /// The process ID to clear + public void ClearProcess(ulong pid) + { + lock (_pidLock) + { + if (_processesByPid.TryRemove(pid, out _)) + { + if (_latestPid == pid) + { + _latestPid = 0; + TitleIDs.CurrentApplication.Value = null; + } + } + } + } + + /// + /// Clears all processes from the ProcessLoader's tracking. + /// This should be called during system shutdown. + /// + public void ClearAllProcesses() + { + lock (_pidLock) + { + _processesByPid.Clear(); + _latestPid = 0; + TitleIDs.CurrentApplication.Value = null; + } + } } } diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs index d6e492317..66bdd57ef 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs @@ -4,7 +4,6 @@ using LibHac.Ns; using Ryujinx.Common.Logging; using Ryujinx.Cpu; using Ryujinx.HLE.HOS.SystemState; -using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.Horizon.Common; namespace Ryujinx.HLE.Loaders.Processes @@ -52,6 +51,7 @@ namespace Ryujinx.HLE.Loaders.Processes if (metaLoader is not null) { + Logger.Info?.Print(LogClass.Application,$"metaLoader: {metaLoader}"); ulong programId = metaLoader.ProgramId; Name = ApplicationControlProperties.Title[(int)titleLanguage].NameString.ToString(); @@ -71,8 +71,15 @@ namespace Ryujinx.HLE.Loaders.Processes ProgramId = programId; ProgramIdText = $"{programId:x16}"; Is64Bit = metaLoader.IsProgram64Bit; + } + + else + { + Logger.Error?.Print(LogClass.Application,$"metaLoader is null !!!"); + ProcessId = 0; + return; } - + DiskCacheEnabled = diskCacheEnabled; AllowCodeMemoryForJit = allowCodeMemoryForJit; } diff --git a/src/Ryujinx.HLE/Ryujinx.HLE.csproj b/src/Ryujinx.HLE/Ryujinx.HLE.csproj index 7e4c8a9e1..f7641c68d 100644 --- a/src/Ryujinx.HLE/Ryujinx.HLE.csproj +++ b/src/Ryujinx.HLE/Ryujinx.HLE.csproj @@ -27,7 +27,9 @@ - + + + diff --git a/src/Ryujinx.HLE/Switch.cs b/src/Ryujinx.HLE/Switch.cs index 850c8b5fa..90af47988 100644 --- a/src/Ryujinx.HLE/Switch.cs +++ b/src/Ryujinx.HLE/Switch.cs @@ -183,6 +183,7 @@ namespace Ryujinx.HLE { if (disposing) { + Processes.ClearAllProcesses(); System.Dispose(); AudioDeviceDriver.Dispose(); FileSystem.Dispose(); diff --git a/src/Ryujinx.Input.SDL3/NpadHdRumble.cs b/src/Ryujinx.Input.SDL3/NpadHdRumble.cs new file mode 100644 index 000000000..408b5213b --- /dev/null +++ b/src/Ryujinx.Input.SDL3/NpadHdRumble.cs @@ -0,0 +1,203 @@ +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Hid; +using SDL; +using static SDL.SDL3; +using System; + +namespace Ryujinx.Input.SDL3 +{ + /// + /// Manages a HID handle of a gamepad to encode and write HD rumble commands for Nin controllers. + /// + public unsafe class NpadHdRumble : IDisposable + { + private readonly SDL_hid_device* _hidHandle; + + private int _globalCount; + private ulong _lastWriteTicks; + + private NpadHdRumble(SDL_hid_device* hidHandle) + { + _hidHandle = hidHandle; + } + + public static NpadHdRumble Create(SDL_Gamepad* gamepadHandle) + { + ushort vendor = SDL_GetGamepadVendor(gamepadHandle); + if (vendor != 0x057e) + { + return null; + } + + ushort product = SDL_GetGamepadProduct(gamepadHandle); + if (!Enum.IsDefined(typeof(HDRumbleSupported), product)) + { + return null; + } + + return new NpadHdRumble(SDL_hid_open(vendor, product, 0)); + } + + // Some of the code was translated from https://github.com/MIZUSHIKI/JoyShockLibrary-plus-HDRumble + private bool WriteHdRumble( + int encLeftLowFreq, int encLeftLowAmp, + int encLeftHighFreq, int encLeftHighAmp, + int encRightLowFreq, int encRightLowAmp, + int encRightHighFreq, int encRightHighAmp) + { + byte[] buf = new byte[10]; + + buf[0] = 0x10; + buf[1] = (byte)((++_globalCount) & 0xF); + + buf[2] = (byte)(encLeftHighFreq & 0xFF); + buf[3] = (byte)(encLeftHighAmp + ((encLeftHighFreq >> 8) & 0xFF)); + buf[4] = (byte)(encLeftLowFreq + ((encLeftLowAmp >> 8) & 0xFF)); + buf[5] = (byte)(encLeftLowAmp & 0xFF); + + buf[6] = (byte)(encRightHighFreq & 0xFF); + buf[7] = (byte)(encRightHighAmp + ((encRightHighFreq >> 8) & 0xFF)); + buf[8] = (byte)(encRightLowFreq + ((encRightLowAmp >> 8) & 0xFF)); + buf[9] = (byte)(encRightLowAmp & 0xFF); + + if (_globalCount > 0xF) + { + _globalCount = 0x0; + } + + fixed (byte* ptr = buf) + { + if (SendHDRumble(ptr, (nuint)buf.Length) >= 0) + { + return true; + } + + if (!String.IsNullOrEmpty(SDL_GetError())) + { + Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError()); + SDL_ClearError(); + } + return false; + } + } + + private static int EncodeLowFreq(float lowFreq) + { + float lf = Math.Clamp(lowFreq, 40.875885f, 626.286133f); + return (int) Math.Round(32 * Math.Log2(lf * 0.1f) - 0x40); + } + + private static int EncodeHighFreq(float highFreq) + { + float hf = Math.Clamp(highFreq, 81.75177f, 1252.572266f); + return (int) Math.Round((32 * Math.Log2(hf * 0.1f) - 0x60) * 4); + } + + private static int EncodeLowAmp(float rawAmp) + { + double encodedAmp = 0; + + if (rawAmp is > 0 and < 0.012f) + { + encodedAmp = 1; + } + else if (rawAmp is >= 0.012f and < 0.112f) + { + encodedAmp = 4 * Math.Log2(rawAmp * 110f); + } + else if (rawAmp is >= 0.112f and < 0.225f) + { + encodedAmp = 16 * Math.Log2(rawAmp * 17f); + } + else if (rawAmp is >= 0.225f and <= 1f) + { + encodedAmp = 32 * Math.Log2(rawAmp * 8.7f); + } + + return (int)Math.Floor(encodedAmp / 2.0) + 64; + } + + private static int EncodeHighAmp(float rawAmp) + { + double encodedAmp = 0; + + if (rawAmp is > 0 and < 0.012f) + { + encodedAmp = 1; + } + else if (rawAmp is >= 0.012f and < 0.112f) + { + encodedAmp = 4 * Math.Log2(rawAmp * 110f); + } + else if (rawAmp is >= 0.112f and < 0.225f) + { + encodedAmp = 16 * Math.Log2(rawAmp * 17f); + } + else if (rawAmp is >= 0.225f and <= 1f) + { + encodedAmp = 32 * Math.Log2(rawAmp * 8.7f); + } + + return (int) Math.Round(encodedAmp * 2); + } + + public bool HdRumble(VibrationValue left, VibrationValue right) + { + return WriteHdRumble(EncodeLowFreq(left.FrequencyLow), + EncodeLowAmp(left.AmplitudeLow), + EncodeHighFreq(left.FrequencyHigh), + EncodeHighAmp(left.AmplitudeHigh), + EncodeLowFreq(right.FrequencyLow), + EncodeLowAmp(right.AmplitudeLow), + EncodeHighFreq(right.FrequencyHigh), + EncodeHighAmp(right.AmplitudeHigh)); + } + + private int SendHDRumble(byte* data, nuint length) + { + int result = 0; + ulong currentTicks = SDL_GetTicks(); + + // Ditch rumble if we haven't hit the poll-rate yet. + // TODO: figure out a better way to do this + // While the polling check makes the rumble accurate, it also causes it to miss signals. + if ((currentTicks - _lastWriteTicks) < 8) // https://docs.handheldlegend.com/s/progcc-3/doc/lag-comparison-aAR1mV3JLX + { + return result; + } + + SDL_LockJoysticks(); + { + // Fun fact: Mario Kart 8 Deluxe sends rumble packets + // where the amplitude is zero, but the frequency isn't. + result = SDL_hid_write(_hidHandle, data, length); + if (result >= 0) + { + _lastWriteTicks = currentTicks; + } + } + SDL_UnlockJoysticks(); + + return result; + } + + public void Dispose() + { + SDL_hid_close(_hidHandle); + } + } + + public enum HDRumbleSupported : ushort + { + JoyConLeft = 0x2006, + JoyConRight = 0x2007, + JoyconPair = 0x2008, + ProController = 0x2009, + JoyconGrip = 0x200e, + Joycon2Right = 0x2066, + Joycon2Left = 0x2067, + Joycon2Pair = 0x2068, + Switch2ProController = 0x2069, + GamecubeController = 0x2073 + } +} diff --git a/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs b/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs index 4985d8eea..57f2940c8 100644 --- a/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs +++ b/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs @@ -2,6 +2,7 @@ using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid.Controller; using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; +using Ryujinx.HLE.HOS.Services.Hid; using System; using System.Collections.Generic; using System.Numerics; @@ -76,11 +77,14 @@ namespace Ryujinx.Input.SDL3 private SDL_Gamepad* _gamepadHandle; + private NpadHdRumble _hdRumble; + private float _triggerThreshold; public SDL3Gamepad(SDL_Gamepad* gamepadHandle, string driverId) { _gamepadHandle = gamepadHandle; + _hdRumble = NpadHdRumble.Create(gamepadHandle); _buttonsUserMapping = new List(20); Name = SDL_GetGamepadName(_gamepadHandle); @@ -165,6 +169,10 @@ namespace Ryujinx.Input.SDL3 protected virtual void Dispose(bool disposing) { + if (disposing && _hdRumble != null) + { + _hdRumble.Dispose(); + } if (disposing && _gamepadHandle != null) { SDL_CloseGamepad(_gamepadHandle); @@ -184,10 +192,17 @@ namespace Ryujinx.Input.SDL3 _triggerThreshold = triggerThreshold; } - public void Rumble(float lowFrequency, float highFrequency, uint durationMs) + public bool HDRumble(VibrationValue left, VibrationValue right) + { + return _hdRumble?.HdRumble(left, right) ?? false; + } + + public bool Rumble(float lowFrequency, float highFrequency, uint durationMs) { if ((Features & GamepadFeaturesFlag.Rumble) == 0) - return; + { + return false; + } ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue); ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue); @@ -206,6 +221,15 @@ namespace Ryujinx.Input.SDL3 if (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs)) Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller."); } + + if (!String.IsNullOrEmpty(SDL_GetError())) + { + Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError()); + SDL_ClearError(); + return false; + } + + return true; } public Vector3 GetMotionData(MotionInputId inputId) diff --git a/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs b/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs index 5311a256c..e9f11d713 100644 --- a/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs +++ b/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs @@ -1,6 +1,7 @@ using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid.Controller; using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Hid; using System; using System.Collections.Generic; using System.Numerics; @@ -61,6 +62,8 @@ namespace Ryujinx.Input.SDL3 public GamepadFeaturesFlag Features { get; } private SDL_Gamepad* _gamepadHandle; + + private NpadHdRumble _hdRumble; private enum JoyConType { @@ -76,6 +79,7 @@ namespace Ryujinx.Input.SDL3 public SDL3JoyCon(SDL_Gamepad* gamepadHandle, string driverId) { _gamepadHandle = gamepadHandle; + _hdRumble = NpadHdRumble.Create(gamepadHandle); _buttonsUserMapping = new List(10); Name = SDL_GetGamepadName(_gamepadHandle); @@ -139,6 +143,10 @@ namespace Ryujinx.Input.SDL3 protected virtual void Dispose(bool disposing) { + if (disposing && _hdRumble != null) + { + _hdRumble.Dispose(); + } if (disposing && _gamepadHandle != null) { SDL_CloseGamepad(_gamepadHandle); @@ -154,13 +162,20 @@ namespace Ryujinx.Input.SDL3 public void SetTriggerThreshold(float triggerThreshold) { - + // No operations + } + + public bool HDRumble(VibrationValue left, VibrationValue right) + { + return _hdRumble?.HdRumble(left, right) ?? false; } - public void Rumble(float lowFrequency, float highFrequency, uint durationMs) + public bool Rumble(float lowFrequency, float highFrequency, uint durationMs) { if ((Features & GamepadFeaturesFlag.Rumble) == 0) - return; + { + return false; + } ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue); ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue); @@ -179,6 +194,15 @@ namespace Ryujinx.Input.SDL3 if (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs)) Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller."); } + + if (!String.IsNullOrEmpty(SDL_GetError())) + { + Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError()); + SDL_ClearError(); + return false; + } + + return true; } public Vector3 GetMotionData(MotionInputId inputId) diff --git a/src/Ryujinx.Input.SDL3/SDL3JoyConPair.cs b/src/Ryujinx.Input.SDL3/SDL3JoyConPair.cs index 14352e5a4..6114674ad 100644 --- a/src/Ryujinx.Input.SDL3/SDL3JoyConPair.cs +++ b/src/Ryujinx.Input.SDL3/SDL3JoyConPair.cs @@ -1,4 +1,7 @@ +using Gommon; using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Hid; using System.Collections.Generic; using System.Linq; using System.Numerics; @@ -61,7 +64,14 @@ namespace Ryujinx.Input.SDL3 return left.IsPressed(inputId) || right.IsPressed(inputId); } - public void Rumble(float lowFrequency, float highFrequency, uint durationMs) + public bool HDRumble(VibrationValue left, VibrationValue right) + { + // return _hdRumble?.HdRumble(left, right) ?? false; + // TODO: Track rumble and motion on both controllers + return false; + } + + public bool Rumble(float lowFrequency, float highFrequency, uint durationMs) { if (lowFrequency != 0) { @@ -78,6 +88,15 @@ namespace Ryujinx.Input.SDL3 left.Rumble(0, 0, durationMs); right.Rumble(0, 0, durationMs); } + + if (!SDL_GetError().IsNullOrEmpty()) + { + Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError()); + SDL_ClearError(); + return false; + } + + return true; } public void SetConfiguration(InputConfig configuration) diff --git a/src/Ryujinx.Input.SDL3/SDL3Keyboard.cs b/src/Ryujinx.Input.SDL3/SDL3Keyboard.cs index f5da11a19..8b179f43f 100644 --- a/src/Ryujinx.Input.SDL3/SDL3Keyboard.cs +++ b/src/Ryujinx.Input.SDL3/SDL3Keyboard.cs @@ -1,6 +1,7 @@ using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid.Keyboard; using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Hid; using System; using System.Collections.Generic; using System.Numerics; @@ -396,9 +397,14 @@ namespace Ryujinx.Input.SDL3 // No operations } - public void Rumble(float lowFrequency, float highFrequency, uint durationMs) + public bool HDRumble(VibrationValue left, VibrationValue right) { - // No operations + return false; + } + + public bool Rumble(float lowFrequency, float highFrequency, uint durationMs) + { + return false; } public Vector3 GetMotionData(MotionInputId inputId) diff --git a/src/Ryujinx.Input.SDL3/SDL3Mouse.cs b/src/Ryujinx.Input.SDL3/SDL3Mouse.cs index 9fdeb36ab..289a60d85 100644 --- a/src/Ryujinx.Input.SDL3/SDL3Mouse.cs +++ b/src/Ryujinx.Input.SDL3/SDL3Mouse.cs @@ -1,5 +1,6 @@ using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Hid; using System; using System.Drawing; using System.Numerics; @@ -67,7 +68,12 @@ namespace Ryujinx.Input.SDL3 throw new NotImplementedException(); } - public void Rumble(float lowFrequency, float highFrequency, uint durationMs) + public bool HDRumble(VibrationValue left, VibrationValue right) + { + throw new NotImplementedException(); + } + + public bool Rumble(float lowFrequency, float highFrequency, uint durationMs) { throw new NotImplementedException(); } diff --git a/src/Ryujinx.Input/HLE/NpadController.cs b/src/Ryujinx.Input/HLE/NpadController.cs index 29bc973f6..85ca5ffcb 100644 --- a/src/Ryujinx.Input/HLE/NpadController.cs +++ b/src/Ryujinx.Input/HLE/NpadController.cs @@ -5,7 +5,6 @@ using Ryujinx.Common.Configuration.Hid.Controller.Motion; using Ryujinx.Common.Logging; using Ryujinx.HLE.HOS.Services.Hid; using System; -using System.Buffers; using System.Collections.Concurrent; using System.Numerics; using System.Runtime.CompilerServices; @@ -555,23 +554,37 @@ namespace Ryujinx.Input.HLE { if (queue.TryDequeue(out (VibrationValue, VibrationValue) dualVibrationValue)) { - if (_config is StandardControllerInputConfig controllerConfig && controllerConfig.Rumble.EnableRumble) + if (_config is not StandardControllerInputConfig controllerConfig || + !controllerConfig.Rumble.EnableRumble) { - VibrationValue leftVibrationValue = dualVibrationValue.Item1; - VibrationValue rightVibrationValue = dualVibrationValue.Item2; - - float low = Math.Min(1f, (float)((rightVibrationValue.AmplitudeLow * 0.85 + rightVibrationValue.AmplitudeHigh * 0.15) * controllerConfig.Rumble.StrongRumble)); - float high = Math.Min(1f, (float)((leftVibrationValue.AmplitudeLow * 0.15 + leftVibrationValue.AmplitudeHigh * 0.85) * controllerConfig.Rumble.WeakRumble)); - - _gamepad?.Rumble(low, high, uint.MaxValue); - - Logger.Debug?.Print(LogClass.Hid, $"Effect for {controllerConfig.PlayerIndex} " + - $"L.low.amp={leftVibrationValue.AmplitudeLow}, " + - $"L.high.amp={leftVibrationValue.AmplitudeHigh}, " + - $"R.low.amp={rightVibrationValue.AmplitudeLow}, " + - $"R.high.amp={rightVibrationValue.AmplitudeHigh} " + - $"--> ({low}, {high})"); + return; } + + VibrationValue leftVibrationValue = dualVibrationValue.Item1; + VibrationValue rightVibrationValue = dualVibrationValue.Item2; + + leftVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble; + leftVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble; + rightVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble; + rightVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble; + + if (!controllerConfig.Rumble.UseHDRumble || _gamepad?.HDRumble(leftVibrationValue, rightVibrationValue) == false) + { + float low = Math.Min(1f, (float)((rightVibrationValue.AmplitudeLow * 0.85 + rightVibrationValue.AmplitudeHigh * 0.15))); + float high = Math.Min(1f, (float)((leftVibrationValue.AmplitudeLow * 0.15 + leftVibrationValue.AmplitudeHigh * 0.85))); + _gamepad?.Rumble(low, high, 0xFFFFFFFF); + } + + Logger.Debug?.Print(LogClass.Hid, $"Effect for {controllerConfig.PlayerIndex} " + + // Value=value/multiplier * multiplier (result) + $"L.low.amp={leftVibrationValue.AmplitudeLow / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.AmplitudeLow}), " + + $"L.high.amp={leftVibrationValue.AmplitudeHigh / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.AmplitudeHigh}), " + + $"L.low.freq={leftVibrationValue.FrequencyLow / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.FrequencyLow}), " + + $"L.high.freq={leftVibrationValue.FrequencyHigh / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.FrequencyHigh}), " + + $"R.low.amp={rightVibrationValue.AmplitudeLow / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.AmplitudeLow}), " + + $"R.high.amp={rightVibrationValue.AmplitudeHigh / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.AmplitudeHigh}), " + + $"R.low.freq={rightVibrationValue.FrequencyLow / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.FrequencyLow}), " + + $"R.high.freq={rightVibrationValue.FrequencyHigh / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.FrequencyHigh})"); } } } diff --git a/src/Ryujinx.Input/IGamepad.cs b/src/Ryujinx.Input/IGamepad.cs index 945ccfa8b..587fd53c0 100644 --- a/src/Ryujinx.Input/IGamepad.cs +++ b/src/Ryujinx.Input/IGamepad.cs @@ -1,5 +1,6 @@ using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Memory; +using Ryujinx.HLE.HOS.Services.Hid; using System; using System.Numerics; using System.Runtime.CompilerServices; @@ -74,16 +75,23 @@ namespace Ryujinx.Input public void ClearLed() => SetLed(0); + /// + /// Starts an HD vibration effect on the gamepad if available. + /// + /// The vibration data for the left side + /// The vibration data for the right side + bool HDRumble(VibrationValue left, VibrationValue right); + /// /// Starts a rumble effect on the gamepad. /// /// The intensity of the low frequency from 0.0f to 1.0f /// The intensity of the high frequency from 0.0f to 1.0f /// The duration of the rumble effect in milliseconds. - void Rumble(float lowFrequency, float highFrequency, uint durationMs); + bool Rumble(float lowFrequency, float highFrequency, uint durationMs); /// - /// Get a snaphost of the state of the gamepad that is remapped with the informations from the set via . + /// Get a snaphost of the state of the gamepad that is remapped with the information from the set via . /// /// A remapped snaphost of the state of the gamepad. GamepadStateSnapshot GetMappedStateSnapshot(); diff --git a/src/Ryujinx.Tests/HLE/CaptureManagerTests.cs b/src/Ryujinx.Tests/HLE/CaptureManagerTests.cs new file mode 100644 index 000000000..8dd2b070f --- /dev/null +++ b/src/Ryujinx.Tests/HLE/CaptureManagerTests.cs @@ -0,0 +1,187 @@ +using NUnit.Framework; +using Ryujinx.HLE.HOS.Services.Caps; +using Ryujinx.HLE.HOS.Services.Caps.Types; +using SkiaSharp; +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Tests.HLE +{ + public class CaptureManagerTests + { + private const int ScreenshotWidth = 1280; + private const int ScreenshotHeight = 720; + private const int BytesPerPixel = 4; + + private const int ScreenshotDataSize = ScreenshotWidth * ScreenshotHeight * BytesPerPixel; // 0x384000 + private const int PaddedScreenshotDataSize = ScreenshotWidth * 768 * BytesPerPixel; // 0x3C0000 + + [Test] + public void SaveScreenShotRejectsBufferSmallerThan720p() + { + using TempSdCard tempSdCard = new(); + + CaptureManager captureManager = CreateCaptureManager(tempSdCard.Path); + byte[] screenshotData = new byte[ScreenshotDataSize - 1]; + + ResultCode result = captureManager.SaveScreenShot( + screenshotData, + appletResourceUserId: 0, + titleId: 0x0100000000001000, + out _); + + Assert.That(result, Is.EqualTo(ResultCode.NullInputBuffer)); + Assert.That(Directory.Exists(Path.Combine(tempSdCard.Path, "Nintendo", "Album")), Is.False); + } + + [Test] + public void SaveScreenShotAcceptsExact720pBuffer() + { + using TempSdCard tempSdCard = new(); + + CaptureManager captureManager = CreateCaptureManager(tempSdCard.Path); + byte[] screenshotData = CreateTestPattern(ScreenshotDataSize); + + ResultCode result = captureManager.SaveScreenShot( + screenshotData, + appletResourceUserId: 0, + titleId: 0x0100000000001000, + out ApplicationAlbumEntry applicationAlbumEntry); + + string filePath = GetSingleAlbumFile(tempSdCard.Path); + + using SKBitmap bitmap = SKBitmap.Decode(filePath); + + Assert.Multiple(() => + { + Assert.That(result, Is.EqualTo(ResultCode.Success)); + Assert.That(bitmap.Width, Is.EqualTo(ScreenshotWidth)); + Assert.That(bitmap.Height, Is.EqualTo(ScreenshotHeight)); + Assert.That(applicationAlbumEntry.TitleId, Is.EqualTo(0x0100000000001000)); + Assert.That(applicationAlbumEntry.AlbumStorage, Is.EqualTo(AlbumStorage.Sd)); + Assert.That(applicationAlbumEntry.ContentType, Is.EqualTo(ContentType.Screenshot)); + Assert.That(applicationAlbumEntry.Unknown0x1f, Is.EqualTo(1)); + }); + } + + [Test] + public void SaveScreenShotAcceptsBufferLargerThan720p() + { + using TempSdCard tempSdCard = new(); + + CaptureManager captureManager = CreateCaptureManager(tempSdCard.Path); + byte[] screenshotData = CreateTestPattern(PaddedScreenshotDataSize); + + ResultCode result = captureManager.SaveScreenShot( + screenshotData, + appletResourceUserId: 0, + titleId: 0x0100000000001000, + out ApplicationAlbumEntry applicationAlbumEntry); + + string filePath = GetSingleAlbumFile(tempSdCard.Path); + + using SKBitmap bitmap = SKBitmap.Decode(filePath); + + Assert.Multiple(() => + { + Assert.That(result, Is.EqualTo(ResultCode.Success)); + Assert.That(bitmap.Width, Is.EqualTo(ScreenshotWidth)); + Assert.That(bitmap.Height, Is.EqualTo(ScreenshotHeight)); + Assert.That(applicationAlbumEntry.TitleId, Is.EqualTo(0x0100000000001000)); + }); + } + + [Test] + public void SaveScreenShotCreatesUniqueFileNamesForRepeatedSaves() + { + using TempSdCard tempSdCard = new(); + + CaptureManager captureManager = CreateCaptureManager(tempSdCard.Path); + byte[] screenshotData = CreateTestPattern(ScreenshotDataSize); + + ResultCode firstResult = captureManager.SaveScreenShot( + screenshotData, + appletResourceUserId: 0, + titleId: 0x0100000000001000, + out _); + + ResultCode secondResult = captureManager.SaveScreenShot( + screenshotData, + appletResourceUserId: 0, + titleId: 0x0100000000001000, + out _); + + string[] files = Directory.GetFiles( + Path.Combine(tempSdCard.Path, "Nintendo", "Album"), + "*.jpg", + SearchOption.AllDirectories); + + Assert.Multiple(() => + { + Assert.That(firstResult, Is.EqualTo(ResultCode.Success)); + Assert.That(secondResult, Is.EqualTo(ResultCode.Success)); + Assert.That(files, Has.Length.EqualTo(2)); + }); + } + + private static CaptureManager CreateCaptureManager(string sdCardPath) + { + CaptureManager captureManager = (CaptureManager)RuntimeHelpers.GetUninitializedObject(typeof(CaptureManager)); + + typeof(CaptureManager) + .GetField("_sdCardPath", BindingFlags.Instance | BindingFlags.NonPublic) + .SetValue(captureManager, sdCardPath); + + return captureManager; + } + + private static string GetSingleAlbumFile(string sdCardPath) + { + string albumPath = Path.Combine(sdCardPath, "Nintendo", "Album"); + + string[] files = Directory.GetFiles(albumPath, "*.jpg", SearchOption.AllDirectories); + + Assert.That(files, Has.Length.EqualTo(1)); + + return files.Single(); + } + + private static byte[] CreateTestPattern(int size) + { + byte[] data = new byte[size]; + + int pixelCount = size / BytesPerPixel; + + for (int i = 0; i < pixelCount; i++) + { + int x = i % ScreenshotWidth; + int y = i / ScreenshotWidth; + + data[(i * 4) + 0] = (byte)(x & 0xff); + data[(i * 4) + 1] = (byte)(y & 0xff); + data[(i * 4) + 2] = 0x80; + data[(i * 4) + 3] = 0xff; + } + + return data; + } + + private sealed class TempSdCard : IDisposable + { + public string Path { get; } = System.IO.Path.Combine( + TestContext.CurrentContext.WorkDirectory, + "sdcard-" + Guid.NewGuid()); + + public void Dispose() + { + if (Directory.Exists(Path)) + { + Directory.Delete(Path, recursive: true); + } + } + } + } +} diff --git a/src/Ryujinx.Tests/HLE/MiiDatabaseTests.cs b/src/Ryujinx.Tests/HLE/MiiDatabaseTests.cs new file mode 100644 index 000000000..b1a20bb93 --- /dev/null +++ b/src/Ryujinx.Tests/HLE/MiiDatabaseTests.cs @@ -0,0 +1,122 @@ +using System.Reflection; + +using NUnit.Framework; + +using Ryujinx.Cpu; +using Ryujinx.HLE.HOS.Services.Mii; +using Ryujinx.HLE.HOS.Services.Mii.StaticService; +using Ryujinx.HLE.HOS.Services.Mii.Types; + +namespace Ryujinx.Tests.HLE +{ + public class MiiDatabaseTests + { + [Test] + public void UpdateLatestReturnsStoredCharInfo() + { + DatabaseImpl database = new(); + StoreData storedData = StoreData.BuildDefault(new UtilityImpl(new TickSource(19200000)), 0); + MiiDatabaseManager databaseManager = GetDatabaseManager(database); + + NintendoFigurineDatabase figurineDatabase = new(); + figurineDatabase.Format(); + figurineDatabase.Add(storedData); + SetFigurineDatabase(databaseManager, figurineDatabase); + + TestDatabaseService service = new(database); + + CharInfo oldCharInfo = new(); + oldCharInfo.SetFromStoreData(storedData); + oldCharInfo.Height--; + + ResultCode result = service.UpdateLatestForTest(oldCharInfo, SourceFlag.Database, out CharInfo newCharInfo); + + Assert.Multiple(() => + { + Assert.That(result, Is.EqualTo(ResultCode.Success)); + Assert.That(newCharInfo.CreateId, Is.EqualTo(oldCharInfo.CreateId)); + Assert.That(newCharInfo.Height, Is.EqualTo(storedData.CoreData.Height)); + Assert.That(newCharInfo.IsValid(), Is.True); + }); + + } + + [Test] + public void AppendAddsRegularCharInfoToDatabase() + { + DatabaseImpl database = new(); + UtilityImpl utilityImpl = new(new TickSource(19200000)); + SetUtilityImpl(database, utilityImpl); + MiiDatabaseManager databaseManager = GetDatabaseManager(database); + SetFigurineDatabase(databaseManager, CreateFormattedDatabase()); + + StoreData defaultStoreData = StoreData.BuildDefault(utilityImpl, 0); + Assert.Multiple(() => + { + Assert.That(defaultStoreData.CoreData.IsValid(), Is.True); + Assert.That(defaultStoreData.IsValidDataCrc(), Is.True); + Assert.That(defaultStoreData.IsValidDeviceCrc(), Is.True); + Assert.That(defaultStoreData.IsValid(), Is.True); + }); + + CharInfo charInfo = new(); + charInfo.SetFromStoreData(defaultStoreData); + + DatabaseSessionMetadata metadata = database.CreateSessionMetadata(new SpecialMiiKeyCode()); + + ResultCode result = databaseManager.Append(metadata, utilityImpl, charInfo); + + int count = databaseManager.GetCount(metadata); + databaseManager.Get(metadata, 0, out StoreData storedData); + + CoreData expectedCoreData = new(); + expectedCoreData.SetFromCharInfo(charInfo); + + Assert.Multiple(() => + { + Assert.That(result, Is.EqualTo(ResultCode.Success)); + Assert.That(count, Is.EqualTo(1)); + Assert.That(storedData.IsValid(), Is.True); + Assert.That(storedData.CreateId, Is.Not.EqualTo(charInfo.CreateId)); + Assert.That(storedData.CoreData, Is.EqualTo(expectedCoreData)); + }); + } + + private sealed class TestDatabaseService(DatabaseImpl database) : DatabaseServiceImpl(database, true, new SpecialMiiKeyCode()) + { + public ResultCode UpdateLatestForTest(CharInfo oldCharInfo, SourceFlag flag, out CharInfo newCharInfo) + { + return UpdateLatest(oldCharInfo, flag, out newCharInfo); + } + } + + private static MiiDatabaseManager GetDatabaseManager(DatabaseImpl database) + { + return (MiiDatabaseManager)typeof(DatabaseImpl) + .GetField("_miiDatabase", BindingFlags.Instance | BindingFlags.NonPublic) + .GetValue(database); + } + + private static void SetFigurineDatabase(MiiDatabaseManager databaseManager, NintendoFigurineDatabase figurineDatabase) + { + typeof(MiiDatabaseManager) + .GetField("_database", BindingFlags.Instance | BindingFlags.NonPublic) + .SetValue(databaseManager, figurineDatabase); + } + + private static NintendoFigurineDatabase CreateFormattedDatabase() + { + NintendoFigurineDatabase figurineDatabase = new(); + figurineDatabase.Format(); + + return figurineDatabase; + } + + private static void SetUtilityImpl(DatabaseImpl database, UtilityImpl utilityImpl) + { + typeof(DatabaseImpl) + .GetField("_utilityImpl", BindingFlags.Instance | BindingFlags.NonPublic) + .SetValue(database, utilityImpl); + } + } +} diff --git a/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs b/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs index af61b7b63..3574c3061 100644 --- a/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs +++ b/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs @@ -221,6 +221,7 @@ namespace Ryujinx.Headless StrongRumble = 1f, WeakRumble = 1f, EnableRumble = false, + UseHDRumble = true }, }; } diff --git a/src/Ryujinx/Headless/HeadlessRyujinx.cs b/src/Ryujinx/Headless/HeadlessRyujinx.cs index bba505dbb..a6ff5da57 100644 --- a/src/Ryujinx/Headless/HeadlessRyujinx.cs +++ b/src/Ryujinx/Headless/HeadlessRyujinx.cs @@ -254,6 +254,7 @@ namespace Ryujinx.Headless Logger.SetEnable(LogLevel.Trace, option.LoggingEnableTrace); Logger.SetEnable(LogLevel.Guest, !option.LoggingDisableGuest); Logger.SetEnable(LogLevel.AccessLog, option.LoggingEnableFsAccessLog); + Logger.SetEnable(LogLevel.NetLog, option.LoggingEnableFsAccessLog); if (!option.DisableFileLog) { diff --git a/src/Ryujinx/Headless/Options.cs b/src/Ryujinx/Headless/Options.cs index 382294cf7..a2dd6e4d3 100644 --- a/src/Ryujinx/Headless/Options.cs +++ b/src/Ryujinx/Headless/Options.cs @@ -108,6 +108,9 @@ namespace Ryujinx.Headless if (NeedsOverride(nameof(LoggingEnableFsAccessLog))) LoggingEnableFsAccessLog = configurationState.Logger.EnableFsAccessLog; + + if (NeedsOverride(nameof(LoggingEnableNetLog))) + LoggingEnableNetLog = configurationState.Logger.EnableNetLog; if (NeedsOverride(nameof(LoggingGraphicsDebugLevel))) LoggingGraphicsDebugLevel = configurationState.Logger.GraphicsDebugLevel; @@ -370,6 +373,9 @@ namespace Ryujinx.Headless [Option("enable-fs-access-logs", Required = false, Default = false, HelpText = "Enables printing FS access log messages.")] public bool LoggingEnableFsAccessLog { get; set; } + + [Option("enable-net-logs", Required = false, Default = false, HelpText = "Enables printing net log messages.")] + public bool LoggingEnableNetLog { get; set; } [Option("graphics-debug-level", Required = false, Default = GraphicsDebugLevel.None, HelpText = "Change Graphics API debug log level.")] public GraphicsDebugLevel LoggingGraphicsDebugLevel { get; set; } diff --git a/src/Ryujinx/Input/AvaloniaKeyboard.cs b/src/Ryujinx/Input/AvaloniaKeyboard.cs index 031d8b033..704a15ba7 100644 --- a/src/Ryujinx/Input/AvaloniaKeyboard.cs +++ b/src/Ryujinx/Input/AvaloniaKeyboard.cs @@ -1,6 +1,7 @@ using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid.Keyboard; using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Hid; using Ryujinx.Input; using System; using System.Collections.Generic; @@ -149,9 +150,20 @@ namespace Ryujinx.Ava.Input Logger.Info?.Print(LogClass.UI, "SetLed called on an AvaloniaKeyboard"); } - public void SetTriggerThreshold(float triggerThreshold) { } + public void SetTriggerThreshold(float triggerThreshold) + { + // No operations + } - public void Rumble(float lowFrequency, float highFrequency, uint durationMs) { } + public bool HDRumble(VibrationValue left, VibrationValue right) + { + return false; + } + + public bool Rumble(float lowFrequency, float highFrequency, uint durationMs) + { + return false; + } public Vector3 GetMotionData(MotionInputId inputId) => Vector3.Zero; diff --git a/src/Ryujinx/Input/AvaloniaMouse.cs b/src/Ryujinx/Input/AvaloniaMouse.cs index 52a341a01..8c449b9ee 100644 --- a/src/Ryujinx/Input/AvaloniaMouse.cs +++ b/src/Ryujinx/Input/AvaloniaMouse.cs @@ -1,5 +1,6 @@ using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Hid; using Ryujinx.Input; using System; using System.Drawing; @@ -64,8 +65,13 @@ namespace Ryujinx.Ava.Input { throw new NotImplementedException(); } + + public bool HDRumble(VibrationValue left, VibrationValue right) + { + throw new NotImplementedException(); + } - public void Rumble(float lowFrequency, float highFrequency, uint durationMs) + public bool Rumble(float lowFrequency, float highFrequency, uint durationMs) { throw new NotImplementedException(); } diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index 2317d67d2..9992cc4b8 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -24,11 +24,9 @@ using Ryujinx.Headless; using Ryujinx.SDL3.Common; using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Security.Principal; -using System.Text; using System.Threading.Tasks; namespace Ryujinx.Ava @@ -48,28 +46,13 @@ namespace Ryujinx.Ava private const uint MbIconwarning = 0x30; + [STAThread] public static int Main(string[] args) { Version = ReleaseInformation.Version; if (OperatingSystem.IsWindows()) { -#if !DEBUG - // this fixes the "hide console" option by forcing the emulator to launch in an old-school cmd - if (!Console.Title.Contains("conhost.exe")) - { - StringBuilder sb = new(); - - foreach (string arg in args) - { - sb.Append(arg.Contains(' ') ? $" \"{arg}\"" : $" {arg}"); - } - - Process.Start("conhost.exe", $"{Environment.ProcessPath} {sb}"); - return 0; - } -#endif - if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041)) { Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run on an outdated version of Windows. Exiting..."); @@ -189,7 +172,7 @@ namespace Ryujinx.Ava CoreDumpArg = coreDumpArg; // TODO: Ryujinx causes core dumps on Linux when it exits "uncleanly", eg. through an unhandled exception. - // This is undesirable and causes very odd behavior during development (the process stops responding, + // This is undesirable and causes very odd behavior during development (the process stops responding, // the .NET debugger freezes or suddenly detaches, /tmp/ gets filled etc.), unless explicitly requested by the user. // This needs to be investigated, but calling prctl() is better than modifying system-wide settings or leaving this be. if (!coreDumpArg) @@ -346,7 +329,7 @@ namespace Ryujinx.Ava ConfigurationPath = appDataConfigurationPath; } } - + if (ConfigurationPath == null) { // No configuration, we load the default values and save it to disk @@ -417,28 +400,28 @@ namespace Ryujinx.Ava _ => ConfigurationState.Instance.HideCursor }; - // Check if memoryManagerMode was overridden. + // Check if memoryManagerMode was overridden. if (CommandLineState.OverrideMemoryManagerMode is not null) if (Enum.TryParse(CommandLineState.OverrideMemoryManagerMode, true, out MemoryManagerMode result)) { ConfigurationState.Instance.System.MemoryManagerMode.Value = result; } - // Check if PPTC was overridden. + // Check if PPTC was overridden. if (CommandLineState.OverridePPTC is not null) if (Enum.TryParse(CommandLineState.OverridePPTC, true, out bool result)) { ConfigurationState.Instance.System.EnablePtc.Value = result; } - // Check if region was overridden. + // Check if region was overridden. if (CommandLineState.OverrideSystemRegion is not null) if (Enum.TryParse(CommandLineState.OverrideSystemRegion, true, out Region result)) { ConfigurationState.Instance.System.Region.Value = result; } - //Check if language was overridden. + //Check if language was overridden. if (CommandLineState.OverrideSystemLanguage is not null) if (Enum.TryParse(CommandLineState.OverrideSystemLanguage, true, out Language result)) { diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index 926af7f50..8a89f3a46 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -28,14 +28,14 @@ true partial - + - + true @@ -49,6 +49,9 @@ + + + @@ -58,9 +61,9 @@ - + - + @@ -70,7 +73,7 @@ - + diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs index 4b1e9cdb5..358b585f4 100644 --- a/src/Ryujinx/Systems/AppHost.cs +++ b/src/Ryujinx/Systems/AppHost.cs @@ -62,7 +62,7 @@ using VSyncMode = Ryujinx.Common.Configuration.VSyncMode; namespace Ryujinx.Ava.Systems { - internal class AppHost + internal class AppHost : IDisposable { private const int CursorHideIdleTime = 5; // Hide Cursor seconds. private const float MaxResolutionScale = 4.0f; // Max resolution hotkeys can scale to before wrapping. @@ -438,7 +438,7 @@ namespace Ryujinx.Ava.Systems SaveBitmapAsPng(bitmapToSave, path); - Logger.Notice.Print(LogClass.Application, $"Screenshot saved to {path}", "Screenshot"); + Logger.Notice.Print(LogClass.Application, $"Screenshot saved to '{path}'.", "Screenshot"); } }); } @@ -611,27 +611,40 @@ namespace Ryujinx.Ava.Systems _isActive = false; - // NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose. - // We only need to wait for all commands submitted during the main gpu loop to be processed. - _gpuDoneEvent.WaitOne(); - _gpuDoneEvent.Dispose(); - DisplaySleep.Restore(); NpadManager.Dispose(); TouchScreenManager.Dispose(); Device.Dispose(); + + // NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose. + // We only need to wait for all commands submitted during the main gpu loop to be processed. + // If the GPU has no work and is cancelled, we need to handle that as well. + WaitHandle.WaitAny(new[] { _gpuDoneEvent, _gpuCancellationTokenSource.Token.WaitHandle }); + + if (_renderingStarted) + { + // Waiting for work to be finished before we dispose. + Device.Gpu.WaitUntilGpuReady(); + } + + _gpuDoneEvent.Dispose(); + _gpuCancellationTokenSource.Dispose(); + DisposeGpu(); - AppExit?.Invoke(this, EventArgs.Empty); } - private void Dispose() + // MUST be public to inherit from IDisposable + public void Dispose() { if (Device.Processes != null) - MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication.ProgramIdText, _playTimer.Elapsed); - + { + MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication?.ProgramIdText, + _playTimer.Elapsed); + } + ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState; ConfigurationState.Instance.Graphics.AspectRatio.Event -= UpdateAspectRatioState; ConfigurationState.Instance.System.EnableDockedMode.Event -= UpdateDockedModeState; @@ -646,7 +659,6 @@ namespace Ryujinx.Ava.Systems _topLevel.PointerExited -= TopLevel_PointerExited; _gpuCancellationTokenSource.Cancel(); - _gpuCancellationTokenSource.Dispose(); _chrono.Stop(); _playTimer.Stop(); @@ -672,6 +684,12 @@ namespace Ryujinx.Ava.Systems } else { + // No use waiting on something that never started work + if (_renderingStarted) + { + Device.Gpu.WaitUntilGpuReady(); + } + Device.DisposeGpu(); } } @@ -686,7 +704,7 @@ namespace Ryujinx.Ava.Systems _cursorState = CursorStates.ForceChangeCursor; } - public async Task LoadGuestApplication(BlitStruct? customNacpData = null) + public async Task LoadGuestApplication(CancellationTokenSource cts, BlitStruct? customNacpData = null) { DiscordIntegrationModule.GuestAppStartedAt = Timestamps.Now; @@ -715,7 +733,8 @@ namespace Ryujinx.Ava.Systems await UserErrorDialog.ShowUserErrorDialog(userError); Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } } @@ -724,10 +743,11 @@ namespace Ryujinx.Ava.Systems await UserErrorDialog.ShowUserErrorDialog(userError); Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } - // Tell the user that we installed a firmware for them. + // Tell the user that we installed firmware for them. if (userError is UserError.NoFirmware) { firmwareVersion = ContentManager.GetCurrentFirmwareVersion(); @@ -747,7 +767,8 @@ namespace Ryujinx.Ava.Systems await UserErrorDialog.ShowUserErrorDialog(userError); Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } } } @@ -762,7 +783,8 @@ namespace Ryujinx.Ava.Systems { Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } } else if (Directory.Exists(ApplicationPath)) @@ -782,20 +804,24 @@ namespace Ryujinx.Ava.Systems if (!Device.LoadCart(ApplicationPath, romFsFiles[0])) { + await ContentDialogHelper.CreateErrorDialog( + "Please specify an unpacked game directory with a valid exefs or NSO/NRO."); Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } } else { Logger.Info?.Print(LogClass.Application, "Loading as cart WITHOUT RomFS."); - if (!Device.LoadCart(ApplicationPath)) { + await ContentDialogHelper.CreateErrorDialog( + "Please specify an unpacked game directory with a valid exefs or NSO/NRO."); Device.Dispose(); - - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } } } @@ -813,7 +839,8 @@ namespace Ryujinx.Ava.Systems { Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } break; @@ -826,7 +853,8 @@ namespace Ryujinx.Ava.Systems { Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } break; @@ -840,7 +868,8 @@ namespace Ryujinx.Ava.Systems { Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } break; @@ -855,7 +884,8 @@ namespace Ryujinx.Ava.Systems { Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } } catch (ArgumentOutOfRangeException) @@ -864,7 +894,8 @@ namespace Ryujinx.Ava.Systems Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } break; @@ -873,19 +904,18 @@ namespace Ryujinx.Ava.Systems } else { - Logger.Warning?.Print(LogClass.Application, "Please specify a valid XCI/NCA/NSP/PFS0/NRO file."); + Logger.Warning?.Print(LogClass.Application, "Please specify a valid XCI/NCA/NSP/PFS0/NSO/NRO file."); Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, appMetadata => appMetadata.UpdatePreGame() ); _playTimer.Start(); - - return true; } internal void Resume() @@ -895,7 +925,7 @@ namespace Ryujinx.Ava.Systems _viewModel.IsPaused = false; _playTimer.Start(); _viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI); - Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed"); + Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed."); } internal void Pause() @@ -905,7 +935,7 @@ namespace Ryujinx.Ava.Systems _viewModel.IsPaused = true; _playTimer.Stop(); _viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI, LocaleManager.Instance[LocaleKeys.Paused]); - Logger.Info?.Print(LogClass.Emulation, "Emulation was paused"); + Logger.Info?.Print(LogClass.Emulation, "Emulation was paused."); } private void InitEmulatedSwitch() @@ -1065,49 +1095,56 @@ namespace Ryujinx.Ava.Systems Device.Gpu.Renderer.RunLoop(() => { - Device.Gpu.SetGpuThread(); - Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token); - - _renderer.Window.ChangeVSyncMode(Device.VSyncMode); - - while (_isActive) + try { - _ticks += _chrono.ElapsedTicks; + Device.Gpu.SetGpuThread(); + Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token); - _chrono.Restart(); + _renderer.Window.ChangeVSyncMode(Device.VSyncMode); - if (Device.WaitFifo()) + while (_isActive) { - Device.Statistics.RecordFifoStart(); - Device.ProcessFrame(); - Device.Statistics.RecordFifoEnd(); - } + _ticks += _chrono.ElapsedTicks; - while (Device.ConsumeFrameAvailable()) - { - if (!_renderingStarted) + _chrono.Restart(); + + if (Device.WaitFifo()) { - _renderingStarted = true; - _viewModel.SwitchToRenderer(false); - InitStatus(); + Device.Statistics.RecordFifoStart(); + Device.ProcessFrame(); + Device.Statistics.RecordFifoEnd(); } - Device.PresentFrame(() => (RendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.SwapBuffers()); - } + while (Device.ConsumeFrameAvailable()) + { + if (!_renderingStarted) + { + _renderingStarted = true; + _viewModel.SwitchToRenderer(false); + InitStatus(); + } - if (_ticks >= _ticksPerFrame) - { - UpdateStatus(); + Device.PresentFrame(() => + (RendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.SwapBuffers()); + } + + if (_ticks >= _ticksPerFrame) + { + UpdateStatus(); + } } } - - // Make sure all commands in the run loop are fully executed before leaving the loop. - if (Device.Gpu.Renderer is ThreadedRenderer threaded) + finally { - threaded.FlushThreadedCommands(); + // Make sure all commands in the run loop are fully executed before leaving the loop. + if (Device.Gpu.Renderer is ThreadedRenderer threaded) + { + Logger.Info?.PrintMsg(LogClass.Gpu, "Flushing threaded commands..."); + threaded.FlushThreadedCommands(); + Logger.Info?.PrintMsg(LogClass.Gpu, "Flushed!"); + } + _gpuDoneEvent.Set(); } - - _gpuDoneEvent.Set(); }); (RendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.MakeCurrent(true); diff --git a/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs b/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs index a36ba7f30..a397e48cc 100644 --- a/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs +++ b/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs @@ -849,7 +849,8 @@ namespace Ryujinx.Ava.Systems.AppLibrary foreach (ApplicationData installedApplication in Applications.Items) { - temporary += LoadAndSaveMetaData(installedApplication.IdString).TimePlayed; + // this should always exist... should... + temporary += LoadAndSaveMetaData(installedApplication.IdString).Value.TimePlayed; } TotalTimePlayed = temporary; @@ -1159,15 +1160,22 @@ namespace Ryujinx.Ava.Systems.AppLibrary ApplicationCountUpdated?.Invoke(null, e); } - public static ApplicationMetadata LoadAndSaveMetaData(string titleId, Action modifyFunction = null) + public static Gommon.Optional LoadAndSaveMetaData(string titleId, Action modifyFunction = null) { + if (titleId is null) + { + Logger.Warning?.PrintMsg(LogClass.Application, "Cannot save metadata because title ID is invalid."); + return null; + } + string metadataFolder = Path.Combine(AppDataManager.GamesDirPath, titleId, "gui"); string metadataFile = Path.Combine(metadataFolder, "metadata.json"); ApplicationMetadata appMetadata; - + if (!File.Exists(metadataFile)) { + Logger.Info?.Print(LogClass.Application, $"Metadata file does not exist. Creating metadata for {titleId}..."); Directory.CreateDirectory(metadataFolder); appMetadata = new ApplicationMetadata(); @@ -1177,12 +1185,12 @@ namespace Ryujinx.Ava.Systems.AppLibrary try { + Logger.Debug?.Print(LogClass.Application, $"Deserializing metadata for {titleId}..."); appMetadata = JsonHelper.DeserializeFromFile(metadataFile, _serializerContext.ApplicationMetadata); } catch (JsonException) { Logger.Warning?.Print(LogClass.Application, $"Failed to parse metadata json for {titleId}. Loading defaults."); - appMetadata = new ApplicationMetadata(); } diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs index f0fafb4e0..1fe98ee69 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs @@ -17,7 +17,7 @@ namespace Ryujinx.Ava.Systems.Configuration /// /// The current version of the file format /// - public const int CurrentVersion = 71; + public const int CurrentVersion = 73; /// /// Version of the configuration file format @@ -113,6 +113,11 @@ namespace Ryujinx.Ava.Systems.Configuration /// Enables printing FS access log messages /// public bool LoggingEnableFsAccessLog { get; set; } + + /// + /// Enables printing network log messages + /// + public bool LoggingEnableNetLog { get; set; } /// /// Enables log messages from Avalonia diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs index 163b7e98f..728321985 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs @@ -1,4 +1,4 @@ -using Avalonia.Media; + using Avalonia.Media; using Gommon; using Ryujinx.Ava.Systems.Configuration.System; using Ryujinx.Ava.Systems.Configuration.UI; @@ -68,6 +68,7 @@ namespace Ryujinx.Ava.Systems.Configuration Logger.EnableTrace.Value = cff.LoggingEnableTrace; Logger.EnableGuest.Value = cff.LoggingEnableGuest; Logger.EnableFsAccessLog.Value = cff.LoggingEnableFsAccessLog; + Logger.EnableNetLog.Value = cff.LoggingEnableNetLog; Logger.FilteredClasses.Value = cff.LoggingFilteredClasses; Logger.GraphicsDebugLevel.Value = cff.LoggingGraphicsDebugLevel; @@ -332,6 +333,7 @@ namespace Ryujinx.Ava.Systems.Configuration EnableRumble = false, StrongRumble = 1f, WeakRumble = 1f, + UseHDRumble = true }; } } diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs index 2b4c8f991..7775125d4 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs @@ -257,6 +257,11 @@ namespace Ryujinx.Ava.Systems.Configuration /// Enables printing FS access log messages /// public ReactiveObject EnableFsAccessLog { get; private set; } + + /// + /// Enables printing network log messages + /// + public ReactiveObject EnableNetLog { get; private set; } /// /// Enables log messages from Avalonia @@ -289,6 +294,7 @@ namespace Ryujinx.Ava.Systems.Configuration EnableTrace = new ReactiveObject(); EnableGuest = new ReactiveObject(); EnableFsAccessLog = new ReactiveObject(); + EnableNetLog = new ReactiveObject(); EnableAvaloniaLog = new ReactiveObject(); FilteredClasses = new ReactiveObject(); EnableFileLog = new ReactiveObject(); diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.cs index 0e2f6aaec..e4874963d 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.cs @@ -47,6 +47,7 @@ namespace Ryujinx.Ava.Systems.Configuration LoggingEnableTrace = Logger.EnableTrace, LoggingEnableGuest = Logger.EnableGuest, LoggingEnableFsAccessLog = Logger.EnableFsAccessLog, + LoggingEnableNetLog = Logger.EnableNetLog, LoggingEnableAvalonia = Logger.EnableAvaloniaLog, LoggingFilteredClasses = Logger.FilteredClasses, LoggingGraphicsDebugLevel = Logger.GraphicsDebugLevel, @@ -176,6 +177,7 @@ namespace Ryujinx.Ava.Systems.Configuration Logger.EnableTrace.Value = false; Logger.EnableGuest.Value = true; Logger.EnableFsAccessLog.Value = false; + Logger.EnableNetLog.Value = false; Logger.EnableAvaloniaLog.Value = false; Logger.FilteredClasses.Value = []; Logger.GraphicsDebugLevel.Value = GraphicsDebugLevel.None; diff --git a/src/Ryujinx/Systems/Configuration/LoggerModule.cs b/src/Ryujinx/Systems/Configuration/LoggerModule.cs index e3d08ab8c..29c38b3d2 100644 --- a/src/Ryujinx/Systems/Configuration/LoggerModule.cs +++ b/src/Ryujinx/Systems/Configuration/LoggerModule.cs @@ -26,6 +26,8 @@ namespace Ryujinx.Ava.Systems.Configuration (_, e) => Logger.SetEnable(LogLevel.Guest, e.NewValue); ConfigurationState.Instance.Logger.EnableFsAccessLog.Event += (_, e) => Logger.SetEnable(LogLevel.AccessLog, e.NewValue); + ConfigurationState.Instance.Logger.EnableNetLog.Event += + (_, e) => Logger.SetEnable(LogLevel.NetLog, e.NewValue); ConfigurationState.Instance.Logger.FilteredClasses.Event += (_, e) => { diff --git a/src/Ryujinx/Systems/DiscordIntegrationModule.cs b/src/Ryujinx/Systems/DiscordIntegrationModule.cs index 5b61340b6..da6371682 100644 --- a/src/Ryujinx/Systems/DiscordIntegrationModule.cs +++ b/src/Ryujinx/Systems/DiscordIntegrationModule.cs @@ -82,7 +82,7 @@ namespace Ryujinx.Ava.Systems public static void Use(Optional titleId) { - if (titleId.TryGet(out string tid)) + if (titleId.TryGet(out string tid) && Switch.Shared.Processes.ActiveApplication is not null) SwitchToPlayingState( ApplicationLibrary.LoadAndSaveMetaData(tid), Switch.Shared.Processes.ActiveApplication diff --git a/src/Ryujinx/Systems/PlayReport/PlayReports.Formatters.cs b/src/Ryujinx/Systems/PlayReport/PlayReports.Formatters.cs index 5aeb923da..649c3cad6 100644 --- a/src/Ryujinx/Systems/PlayReport/PlayReports.Formatters.cs +++ b/src/Ryujinx/Systems/PlayReport/PlayReports.Formatters.cs @@ -1,5 +1,6 @@ using Gommon; using Humanizer; +using MsgPack; using System; using System.Buffers.Binary; using System.Collections.Generic; @@ -23,24 +24,382 @@ namespace Ryujinx.Ava.Systems.PlayReport private static FormattedValue SkywardSwordHD_Rupees(SingleValue value) => "rupee".ToQuantity(value.Matched.IntValue); - private static FormattedValue SuperMarioOdyssey_AssistMode(SingleValue value) - => value.Matched.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode"; + private static FormattedValue EchoesOfWisdom_Warp(SingleValue value) + { + FormattedValue locations = value.Matched.IntValue switch + { + // Hyrule Field + 23 => "Hyrule Field: Kakariko Village", + 43 => "Hyrule Field: West of Hyrule Ranch", + 45 => "Hyrule Field: North of Hyrule Ranch", + 25 => "Hyrule Field: Hyrule Ranch", + 26 => "Hyrule Field: West of Hyrule Castle", + 48 => "Hyrule Field: Haunted Grove", + 24 => "Hyrule Field: Hyrule Castle", + 27 => "Hyrule Field: Northern Sanctuary", + 28 => "Eastern Hyrule Field: Eastern Temple", + 41 => "Eastern Hyrule Field: Dampé Studio", + 22 => "Lake Hylia: Great Fairy Shrine", + // Eternal Forest + 47 => "Eternal Forest: Entrance", + 46 => "Eternal Forest: Great Deku Tree", + 752 => "Eternal Forest: Stilled Ancient Ruins (Halfway Point)", + 753 => "Eternal Forest: Stilled Ancient Ruins (Null)", + // Suthorn + 33 => "Suthorn Prairie: Lueburry's House", + 20 => "Suthorn Prairie: Suthorn Village", + 21 => "Suthorn Forest: Suthorn Ruins", + // Faron Wetlands + 13 => "Faron Wetlands: Entrance", + 15 => "Faron Wetlands: Scrubton", + 18 => "Faron Wetlands: Blossu's House", + 17 => "Faron Wetlands: Heart Lake", + 852 => "Faron Wetlands: Stilled Faron Wetlands", + 601 => "Faron Wetlands: Faron Temple 3F", + 602 => "Faron Wetlands: Faron Temple 2F (Underwater Entrance)", + 603 => "Faron Wetlands: Faron Temple 2F (West Entrance)", + 604 => "Faron Wetlands: Faron Temple 2F (Cliff Entrance)", + 605 => "Faron Wetlands: Faron Temple 1F (Diababa)", + 606 => "Faron Wetlands: Faron Temple 1F (Gohma)", + // Jabul Waters + 11 => "Jabul Waters: River Zora Village", + 9 => "Jabul Waters: Crossflows Plaza", + 8 => "Jabul Waters: Seesyde Village", + 12 => "Jabul Waters: Sea Zora Village", + 10 => "Jabul Waters: Lord Jabu-Jabu's Den", + 201 => "Jabul Waters: Jabul Ruins 1F (Entrance)", + 202 => "Jabul Waters: Jabul Ruins 1F (Vocavor)", + // Gerudo Desert + 40 => "Gerudo Desert: Entrance", + 29 => "Gerudo Desert: Oasis", + 32 => "Gerudo Desert: Ancestor's Cave Of Rest", + 30 => "Gerudo Desert: Gerudo Town", + 31 => "Gerudo Desert: Gerudo Sanctum", + 351 => "Gerudo Desert: Stilled Gerudo Sanctum", + 303 => "Gerudo Desert: Gerudo Sanctum 1F (West Entrance)", + 304 => "Gerudo Desert: Gerudo Sanctum 1F (East Entrance)", + 301 => "Gerudo Desert: Gerudo Sanctum 2F (The Key)", + 302 => "Gerudo Desert: Gerudo Sanctum 2F (The Elephant Room)", + 305 => "Gerudo Desert: Gerudo Sanctum 2F (Mogryph)", + // Eldin Volcano + 4 => "Eldin Volcano: Eldin Volcano Trail", + 44 => "Eldin Volcano: Lava Lake", + 3 => "Eldin Volcano: Goron City", + 5 => "Eldin Volcano: Rock Roast Volcano", + 49 => "Eldin Volcano: Crater Shortcut", + 552 => "Eldin Volcano: Stilled Eldin Volcano", + 501 => "Eldin Volcano: Eldin Temple 1F", + 503 => "Eldin Volcano: Eldin Temple 2F", + 502 => "Eldin Volcano: Eldin Temple 3F", + // Hebra Mountain + 34 => "Hebra Mountain: Hebra Mountain Passage (1)", + 35 => "Hebra Mountain: Sheltered Hot Spring", + 36 => "Hebra Mountain: Condé's House", + 38 => "Hebra Mountain: Hebra Mountain Passage (2)", + 37 => "Hebra Mountain: Hebra Mountain Passage (3)", + 39 => "Hebra Mountain: Summit", + 652 => "Hebra Mountain: Stilled Holy Mount Lanayru", + 801 => "Hebra Mountain: Lanayru Temple 1F", + 802 => "Hebra Mountain: Lanayru Temple B2", + 803 => "Hebra Mountain: Lanayru Temple B4", + _ => FormattedValue.ForceReset + }; - private static FormattedValue SuperMarioOdysseyChina_AssistMode(SingleValue value) - => value.Matched.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式"; + return locations.Reset + ? FormattedValue.ForceReset + : $"Warped to {locations}"; + } + + private static FormattedValue SuperMario3DAllStars(SingleValue value) + { + // TODO: Is this really necessary? + FormattedValue title = value.Matched.IntValue switch + { + 1 => "Super Mario 64", + 2 => "Super Mario Sunshine", + 3 => "Super Mario Galaxy", + _ => FormattedValue.ForceReset + }; + + return title.Reset + ? FormattedValue.ForceReset + : $"Playing {title}"; + } + + private static FormattedValue SuperMario3DAllStars_MainMenu(MultiValue value) + { + int albumId = value.Matched[0].IntValue; + int songId = value.Matched[1].IntValue; + + string album = value.Matched[0].IntValue switch + { + 1 => "Super Mario 64 OST", + 2 => "Super Mario Sunshine OST", + 3 => "Super Mario Galaxy OST", + _ => "Listening to Super Mario 3D All-Stars" + }; + + string song = (albumId, songId) switch + { + // Super Mario 64 + (1, 0) => "It's a Me, Mario!", + (1, 1) => "Title Theme", + (1, 2) => "Peach's Message", + (1, 3) => "Opening", + (1, 4) => "Super Mario 64 Main Theme", + (1, 5) => "Slider", + (1, 6) => "Inside the Castle Walls", + (1, 7) => "Looping Steps", + (1, 8) => "Dire, Dire Docks", + (1, 9) => "Lethal Lava Land", + (1, 10) => "Snow Mountain", + (1, 11) => "Haunted House", + (1, 12) => "Merry-Go-Round", + (1, 13) => "Cave Dungeon", + (1, 14) => "Piranha Plant's Lullaby", + (1, 15) => "Powerful Mario", + (1, 16) => "Metallic Mario", + (1, 17) => "File Select", + (1, 18) => "Correct Solution", + (1, 19) => "Toad's Message", + (1, 20) => "Power Star", + (1, 21) => "Race Fanfare", + (1, 22) => "Star Catch Fanfare", + (1, 23) => "Game Start", + (1, 24) => "Course Clear", + (1, 25) => "Game Over", + (1, 26) => "Stage Boss", + (1, 27) => "Koopa's Message", + (1, 28) => "Koopa's Road", + (1, 29) => "Koopa's Theme", + (1, 30) => "Koopa Clear", + (1, 31) => "Ultimate Koopa", + (1, 32) => "Ultimate Koopa Clear", + (1, 33) => "Ending Demo", + (1, 34) => "Staff Roll", + (1, 35) => "Piranha Plant's Lullaby - Piano", + + // Super Mario Sunshine + (2, 0) => "Isle Delfino", + (2, 1) => "Delfino Airstrip", + (2, 2) => "Bianco Hills", + (2, 3) => "Ricco Harbor", + (2, 4) => "Gelato Beach", + (2, 5) => "Pinna Beach", + (2, 6) => "Pinna Park", + (2, 7) => "Sirena Beach", + (2, 8) => "Hotel Delfino", + (2, 9) => "Casino", + (2, 10) => "Noki Bay", + (2, 11) => "Noki Depths", + (2, 12) => "Pianta Village", + (2, 13) => "Pianta Hot Spring", + (2, 14) => "Pianta Rescue", + (2, 15) => "Pianta Village - Fluff Festival", + (2, 16) => "Underground", + (2, 17) => "Secret Course", + (2, 18) => "Secret Course - Sky and Sea", + (2, 19) => "Corona Mountain", + (2, 20) => "Mid-Boss", + (2, 21) => "Proto Piranha", + (2, 22) => "Phantamanta", + (2, 23) => "Boss Battle", + (2, 24) => "Gooper Blooper Intro", + (2, 25) => "Wiggler Intro", + (2, 26) => "Mecha-Bowser", + (2, 27) => "Bowser", + (2, 28) => "Shadow Mario", + (2, 29) => "Racing Il Piantissimo", + (2, 30) => "Event", + (2, 31) => "Timed Event", + (2, 32) => "Yoshi-Go-Round", + (2, 33) => "Title Screen", + (2, 34) => "Opening Demo", + (2, 35) => "Select Data", + (2, 36) => "Select Scenario", + (2, 37) => "Course Intro", + (2, 38) => "Course Intro - Shadow Mario", + (2, 39) => "A Shine Sprite Appears", + (2, 40) => "Shine!", + (2, 41) => "Race Fanfare", + (2, 42) => "Casino Fanfare", + (2, 43) => "Too Bad!", + (2, 44) => "Game Over", + (2, 45) => "Welcome to Isle Delfino (Movie)", + (2, 46) => "Icky Goop (Movie)", + (2, 47) => "Mario on Trial (Movie)", + (2, 48) => "How to Use FLUDD (Movie)", + (2, 49) => "Shadow Mario Appears (Movie)", + (2, 50) => "The Kidnapping of Princess Peach (Movie)", + (2, 51) => "Mecha-Bowser Rises (Movie)", + (2, 52) => "Meet Bowser Jr. (Movie)", + (2, 53) => "FLUDD Theft (Movie)", + (2, 54) => "Hot Tub Intrusion (Movie)", + (2, 55) => "Epilogue (Movie)", + (2, 56) => "Staff Credits", + (2, 57) => "Have a Relaxing Vacation!", + + // Super Mario Galaxy + (3, 0) => "Overture", + (3, 1) => "The Star Festival", + (3, 2) => "Attack of the Airships", + (3, 3) => "Catastrophe", + (3, 4) => "Peach's Castle Stolen", + (3, 5) => "Enter the Galaxy", + (3, 6) => "Egg Planet", + (3, 7) => "Rosaline in the Observatory 1", + (3, 8) => "The Honeyhive", + (3, 9) => "Space Junk Road", + (3, 10) => "Battlerock Galaxy", + (3, 11) => "Beach Bowl Galaxy", + (3, 12) => "Rosalina in the Observatory 2", + (3, 13) => "Enter Bowser Jr.!", + (3, 14) => "Waltz of the Boos", + (3, 15) => "Buoy Base Galaxy", + (3, 16) => "Gusty Garden Galaxy", + (3, 17) => "Rosaline in the Observatory 3", + (3, 18) => "King Bowser", + (3, 19) => "Melty Molten Galaxy", + (3, 20) => "The Galaxy Reactor", + (3, 21) => "Final Battle with Bowser", + (3, 22) => "A New Dawn", + (3, 23) => "Birth", + (3, 24) => "Super Mario Galaxy", + (3, 25) => "Purple Comet", + (3, 26) => "Blue Sky Athletic", + (3, 27) => "Super Mario 2007", + (3, 28) => "File Select", + (3, 29) => "Luma", + (3, 30) => "Gateway Galaxy", + (3, 31) => "Stolen Grand Star", + (3, 32) => "To the Observatory Grounds 1", + (3, 33) => "Observation Dome", + (3, 34) => "Course Select", + (3, 35) => "Dino Piranha", + (3, 36) => "A Chance to Grab a Star!", + (3, 37) => "A Tense Moment", + (3, 38) => "Big Bad Bugaboom", + (3, 39) => "King Kaliente", + (3, 40) => "The Toad Brigade", + (3, 41) => "Airship Armada", + (3, 42) => "Aquatic Race", + (3, 43) => "Space Fantasy", + (3, 44) => "Megaleg", + (3, 45) => "To The Observatory Grounds 2", + (3, 46) => "Space Athletic", + (3, 47) => "Speedy Comet", + (3, 48) => "Beach Bowl Galaxy - Undersea", + (3, 49) => "Interlude", + (3, 50) => "Bowser's Stronghold Appears", + (3, 51) => "The Fiery Stronghold", + (3, 52) => "The Big Staircase", + (3, 53) => "Bowser Appears", + (3, 54) => "Star Ball", + (3, 55) => "The Library", + (3, 56) => "Buoy Base Galaxy - Undersea", + (3, 57) => "Rainbow Mario", + (3, 58) => "Chase the Bunnies", + (3, 59) => "Help!", + (3, 60) => "Major Burrows", + (3, 61) => "Pipe Interior", + (3, 62) => "Cosmic Comet", + (3, 63) => "Drip Drop Galaxy", + (3, 64) => "Kingfin", + (3, 65) => "Boo Race", + (3, 66) => "Ice Mountain", + (3, 67) => "Ice Mario", + (3, 68) => "Lava Path", + (3, 69) => "Fire Mario", + (3, 70) => "Dusty Dune Galaxy", + (3, 71) => "Heavy Metal Mecha-Bowser", + (3, 72) => "A-wa-wa-wa!", + (3, 73) => "Deep Dark Galaxy", + (3, 74) => "Kamella", + (3, 75) => "Star Ball 2", + (3, 76) => "Sad Girl", + (3, 77) => "Flying Mario", + (3, 78) => "Star Child", + (3, 79) => "A Wish", + (3, 80) => "Family", + _ => "" + }; + + return string.IsNullOrEmpty(song) ? FormattedValue.ForceReset : $"{album} - {song}"; + } + + private static FormattedValue SuperMarioOdyssey(SingleValue value) + => value.Matched.LongValue switch + { + // TODO: Needs updated for sub-areas. + 2973331007 => "Cap Kingdom: Bonneton", + 2661781375 => "Cascade Kingdom: Fossil Falls", + 512560049 => "Sand Kingdom: Tostarena", + 3079659402 => "Wooded Kingdom: Steam Gardens", + 1941286268 => "Lake Kingdom: Lake Lamode", + 3098209122 => "Cloud Kingdom: Nimbus Arena", + 4088050842 => "Lost Kingdom: Forgotten Isle", + 53003352 => "Metro Kingdom: New Donk City", + 4265839612 => "Seaside Kingdom: Bubblaine", + 3288863344 => "Snow Kingdom: Shiveria", + 3180104973 => "Luncheon Kingdom: Mount Volbono", + 2284558980 => "Ruined Kingdom: Crumbleden", + 3024139598 => "Bowser's Kingdom: Bowser's Castle", + 1351608174 => "Moon Kingdom: Honeylune Ridge", + 1698750149 => "Dark Side: Rabbit Ridge", + 3206301958 => "Darker Side: Culmina Crater", + 3963002526 => "Mushroom Kingdom: Peach's Castle", + _ => FormattedValue.ForceReset + }; private static FormattedValue SuperMario3DWorldOrBowsersFury(SingleValue value) => value.Matched.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury"; + private static FormattedValue SuperMarioWonder(SingleValue value) + { + // TODO: Needs updated for course names. + MessagePackObject messagePackObject = value.Matched.PackedValue; + MessagePackObjectDictionary messagePackObjectDictionary = messagePackObject.AsDictionary(); + + int worldNumber = messagePackObjectDictionary["world_no"].AsInt32(); + int courseNumber = 0; + + if (messagePackObjectDictionary.TryGetValue("course_no", out MessagePackObject courseNumberVariable)) + { + courseNumber = courseNumberVariable.AsInt32(); + } + + FormattedValue world = worldNumber switch + { + 1 => "Pipe-Rock Plateau", + 2 => "Petal Isles", + 3 => "Fluff-Puff Peaks", + 4 => "Shining Falls", + 5 => "Sunbaked Desert", + 6 => "Fungi Mines", + 7 => "Deep Magma Bog", + 9 => "Special World", + _ => FormattedValue.ForceReset + }; + + if (courseNumber == 0) + { + return FormattedValue.ForceReset; + } + + return world.Reset + ? FormattedValue.ForceReset + : $"{world}: {worldNumber}-{courseNumber}"; + } + private static FormattedValue MarioKart8Deluxe_Mode(SingleValue value) => value.Matched.StringValue switch { // Single Player "Single" => "Single Player", // Multiplayer - "Multi-2players" => "Multiplayer 2 Players", - "Multi-3players" => "Multiplayer 3 Players", - "Multi-4players" => "Multiplayer 4 Players", + "Multi-2players" => "Multiplayer: 2 Players", + "Multi-3players" => "Multiplayer: 3 Players", + "Multi-4players" => "Multiplayer: 4 Players", // Wireless/LAN Play "Local-Single" => "Wireless/LAN Play", "Local-2players" => "Wireless/LAN Play 2 Players", @@ -62,8 +421,9 @@ namespace Ryujinx.Ava.Systems.PlayReport private static FormattedValue PokemonSV(MultiValue values) { - - string playStatus = values.Matched[0].BoxedValue is 0 ? "Playing Alone" : "Playing in a group"; + string region = PokemonSV_Region(values.Matched[1].ToString()); + string union = values.Matched[0].BoxedValue is 0 ? "" : " with friends"; + string academyName = PokemonSV_AcademyName(values.Application.Title); FormattedValue locations = values.Matched[1].ToString() switch { @@ -89,18 +449,86 @@ namespace Ryujinx.Ava.Systems.PlayReport "a_w20" => "North Area Three", "a_w21" => "North Area One", "a_w22" => "North Area Two", - "a_w23" => "The Great Crater of Paldea", + "a_w23" => "Area Zero: The Great Crater of Paldea", "a_w24" => "South Paldean Sea", "a_w25" => "West Paldean Sea", "a_w26" => "East Paldean Sea", "a_w27" => "North Paldean Sea", - //TODO DLC Locations + // Naranja / Uva Academy + "a_sch_entrance01" => $"{academyName} Academy: Entrance", + "a_sch_cafe01" => $"{academyName} Academy: Cafeteria", + "a_sch_shop01" => $"{academyName} Academy: School Store", + "a_sch_room01" => $"{academyName} Academy: Home Ec Room", + "a_sch_room02" => $"{academyName} Academy: Art Room", + "a_sch_room03" => $"{academyName} Academy: Biology Lab", + "a_sch_room04" => $"{academyName} Academy: Staff Room", + "a_sch_office01" => $"{academyName} Academy: Director's Office", + "a_sch_office03" => $"{academyName} Academy: Nurse's Office", + "a_sch_ground01" => $"{academyName} Academy: School Yard", + "a_sch_class1a" => $"{academyName} Academy: Classroom 1-A", + "a_sch_class1d" => $"{academyName} Academy: Classroom 1-D", + "a_sch_class2g" => $"{academyName} Academy: Classroom 2-G", + "a_sch_dorm01" => $"{academyName} Academy: Dorm Room (Trainer)", + "a_sch_dorm02" => $"{academyName} Academy: Dorm Room (Nemona)", + "a_sch_dorm03" => $"{academyName} Academy: Dorm Room (Arven)", + "a_sch_dorm04" => $"{academyName} Academy: Dorm Room (Penny)", + // DLC + // Kitakami + "a_su0101" => "Mossui Town", + "a_su0102" => "Loyalty Plaza", + "a_su0103" => "Kitakami Hall", + "a_su0104" => "Oni Mountain", + "a_su0105" => "Infernal Pass", + "a_su0106" => "Crystal Pool", + "a_su0107" => "Wistful Fields", + "a_su0108" => "Mossfell Confluence", + "a_su0109" => "Fellhorn Gorge", + "a_su0110" => "Paradise Barrens", + "a_su0111" => "Timeless Woods", + // Blueberry Academy: School + "a_sch_2_entrance0" => "Blueberry Academy: Entrance", + "a_sch_2_clubroom" => "Blueberry Academy: League Clubroom", + "a_sch_2_class1" => "Blueberry Academy: Classroom 1-4", + "a_sch_2_class2" => "Blueberry Academy: Classroom 3-2", + "a_sch_2_shop01" => "Blueberry Academy: School Store", + "a_sch_2_cafe01" => "Blueberry Academy: Cafeteria", + "a_sch_2_dorm01" => "Blueberry Academy: Dorm Room (Trainer)", + "a_sch_2_dorm02" => "Blueberry Academy: Dorm Room (Carmine)", + // Blueberry Academy: Terrarium + "a_su0201" => "Savanna Biome", + "a_su0202" => "Coastal Biome", + "a_su0203" => "Canyon Biome", + "a_su0204" => "Polar Biome", _ => FormattedValue.ForceReset }; - - return locations.Reset - ? FormattedValue.ForceReset - : $"{playStatus} in {locations}"; + + return locations.Reset + ? FormattedValue.ForceReset + : $"Exploring {region}{union} | {locations}"; + } + + private static string PokemonSV_Region(string location) + { + if (location.Contains("a_su02") || location.Contains("a_sch_2")) return "Unova"; + if (location.Contains("a_su01")) return "Kitakami"; + return "Paldea"; + } + + private static string PokemonSV_AcademyName(string title) + { + // TODO: Is this even necessary? + if ( + title.Contains("Scarlet") + || title.Contains("Escarlata") + || title.Contains("Écarlate") + || title.Contains("Karmesin") + || title.Contains("Scarlatto") + || title.Contains("スカーレット") + || title.Contains("스칼렛") + || title.Contains("朱") + + ) { return "Naranja"; } + return "Uva"; } private static FormattedValue SuperSmashBrosUltimate_Mode(SparseMultiValue values) @@ -641,5 +1069,7 @@ namespace Ryujinx.Ava.Systems.PlayReport _ => FormattedValue.ForceReset }; + + } } diff --git a/src/Ryujinx/Systems/PlayReport/PlayReports.cs b/src/Ryujinx/Systems/PlayReport/PlayReports.cs index 628194b19..d483515bb 100644 --- a/src/Ryujinx/Systems/PlayReport/PlayReports.cs +++ b/src/Ryujinx/Systems/PlayReport/PlayReports.cs @@ -14,7 +14,7 @@ namespace Ryujinx.Ava.Systems.PlayReport private static readonly Lazy _analyzerLazy = new(() => new Analyzer() .AddSpec( - "01007ef00011e000", + "01007ef00011e000", // Breath of the Wild spec => spec .WithDescription("based on being in Master Mode.") .AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode) @@ -22,48 +22,74 @@ namespace Ryujinx.Ava.Systems.PlayReport .AddValueFormatter("AoCVer", FormattedValue.SingleAlwaysResets) ) .AddSpec( - "0100f2c0115b6000", + "0100f2c0115b6000", // Tears of the Kingdom spec => spec .WithDescription("based on where you are in Hyrule (Depths, Surface, Sky).") .AddValueFormatter("PlayerPosY", TearsOfTheKingdom_CurrentField)) .AddSpec( - "01002da013484000", + "01002da013484000", // Skyward Sword spec => spec .WithDescription("based on how many Rupees you have.") .AddValueFormatter("rupees", SkywardSwordHD_Rupees)) + .AddSpec( - "0100000000010000", + "01008cf01baac000", // Echoes of Wisdom spec => spec - .WithDescription("based on if you're playing with Assist Mode.") - .AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode) + .WithDescription("based on where you've warped.") + .AddValueFormatter("dest_index", EchoesOfWisdom_Warp) + ) + + .AddSpec( + "010049900f546000", // Super Mario 3D All Stars + spec => spec + .WithDescription("based on what album and track you're listening to.") + .AddMultiValueFormatter(["app_id","song_id"], SuperMario3DAllStars_MainMenu) ) .AddSpec( - "010075000ecbe000", + ["010049900f546001", "010049900f546002", "010049900F546003"], // Super Mario 3D All Stars spec => spec - .WithDescription("based on if you're playing with Assist Mode.") - .AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode) + .WithDescription("based on which game you've selected to play in the collection.") + .AddValueFormatter("program_id", SuperMario3DAllStars) ) .AddSpec( - "010028600ebda000", + "0100000000010000", // Super Mario Odyssey + spec => spec + .WithDescription("based on what kingdom you're in.") + .AddValueFormatter("stage_name", SuperMarioOdyssey) + ) + .AddSpec( + "010028600ebda000", // Super Mario 3D World + Bowser's Fury spec => spec .WithDescription("based on being in either Super Mario 3D World or Bowser's Fury.") .AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury) ) + .AddSpec( + ["010049900f546000", "010049900f546001", "010049900f546002", "010049900F546003"], + spec => spec + .WithDescription("based on which game you've selected to play in the collection.") + .AddValueFormatter("program_id", SuperMario3DAllStars) + ) + .AddSpec( + "010015100b514000", // Super Mario Bros. Wonder + spec => spec + .WithDescription("based on what world and course you're in.") + .AddValueFormatter("stage_info", SuperMarioWonder) + ) .AddSpec( // Global & China IDs - ["0100152000022000", "010075100e8ec000"], + ["0100152000022000", "010075100e8ec000"], // Mario Kart 8 Deluxe spec => spec .WithDescription( "based on what modes you're selecting in the menu & whether or not you're in a race.") .AddValueFormatter("To", MarioKart8Deluxe_Mode) ) .AddSpec( - ["0100a3d008c5c000", "01008f6008c5e000"], + ["0100a3d008c5c000", "01008f6008c5e000"], // Pokemon Scarlet/Violet spec => spec .WithDescription("based on if you're playing alone or in a group and what area of Paldea you're exploring.") .AddMultiValueFormatter(["team_circle", "area_no"], PokemonSV) ) .AddSpec( - "01006a800016e000", + "01006a800016e000", // Super Smash Bros. Ultimate spec => spec .WithDescription("based on what mode you're playing, who won, and what characters were present.") .AddSparseMultiValueFormatter( @@ -83,8 +109,10 @@ namespace Ryujinx.Ava.Systems.PlayReport ) .AddSpec( [ - "0100c9a00ece6000", "01008d300c50c000", "0100d870045b6000", - "010012f017576000", "0100c62011050000", "0100b3c014bda000" + "0100B4E00444C000", "0100d870045b6000", "01008d300c50c000", "0100c62011050000", "010012f017576000", + /*Famicom*/ /*NES*/ /*SNES*/ /*GBC*/ /*GBA*/ + "0100b3c014bda000", "0100c9a00ece6000", "0100e0601c632000", "0100bfc01d976000" + /*SEGA Genesis*/ /*N64*/ /*N64 MATURE*/ /*Virtual Boy*/ ], spec => spec .WithDescription( diff --git a/src/Ryujinx/UI/Helpers/Win32NativeInterop.cs b/src/Ryujinx/UI/Helpers/Win32NativeInterop.cs index 0ff7f5fde..adfa899ed 100644 --- a/src/Ryujinx/UI/Helpers/Win32NativeInterop.cs +++ b/src/Ryujinx/UI/Helpers/Win32NativeInterop.cs @@ -8,6 +8,12 @@ namespace Ryujinx.Ava.UI.Helpers internal partial class Win32NativeInterop { internal const int GWLP_WNDPROC = -4; + internal const int GWL_STYLE = -16; + internal const int GWL_EXSTYLE = -20; + + internal const uint WS_OVERLAPPEDWINDOW = 0x00CF0000; + internal const uint WS_POPUP = 0x80000000; + internal const uint WS_VISIBLE = 0x10000000; [Flags] public enum ClassStyles : uint @@ -107,9 +113,29 @@ namespace Ryujinx.Ava.UI.Helpers nint hInstance, nint lpParam); + [LibraryImport("user32.dll", SetLastError = true, EntryPoint = "GetWindowLongPtrW")] + public static partial nint GetWindowLongPtrW(nint hWnd, int nIndex); + [LibraryImport("user32.dll", SetLastError = true)] public static partial nint SetWindowLongPtrW(nint hWnd, int nIndex, nint value); + [LibraryImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool SetWindowPos( + nint hWnd, + nint hWndInsertAfter, + int x, + int y, + int cx, + int cy, + uint uFlags); + + internal const uint SWP_NOZORDER = 0x0004; + internal const uint SWP_NOACTIVATE = 0x0010; + internal const uint SWP_FRAMECHANGED = 0x0020; + internal const uint SWP_NOMOVE = 0x0002; + internal const uint SWP_NOSIZE = 0x0001; + [LibraryImport("user32.dll", SetLastError = true)] public static partial ushort GetAsyncKeyState(int nVirtKey); diff --git a/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs b/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs index 0eeef45f5..2a076f525 100644 --- a/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs +++ b/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs @@ -20,6 +20,7 @@ namespace Ryujinx.Ava.UI.Models.Input public float WeakRumble { get; set; } public float StrongRumble { get; set; } + public bool UseHDRumble { get; set; } public string Id { get; set; } @@ -236,6 +237,7 @@ namespace Ryujinx.Ava.UI.Models.Input EnableRumble = controllerInput.Rumble.EnableRumble; WeakRumble = controllerInput.Rumble.WeakRumble; StrongRumble = controllerInput.Rumble.StrongRumble; + UseHDRumble = controllerInput.Rumble.UseHDRumble; } if (controllerInput.Led != null) @@ -307,6 +309,7 @@ namespace Ryujinx.Ava.UI.Models.Input EnableRumble = EnableRumble, WeakRumble = WeakRumble, StrongRumble = StrongRumble, + UseHDRumble = UseHDRumble, }, Led = new LedConfigController { diff --git a/src/Ryujinx/UI/Models/SaveModel.cs b/src/Ryujinx/UI/Models/SaveModel.cs index d245ed4d9..bd9c93f5d 100644 --- a/src/Ryujinx/UI/Models/SaveModel.cs +++ b/src/Ryujinx/UI/Models/SaveModel.cs @@ -57,8 +57,15 @@ namespace Ryujinx.Ava.UI.Models } else { - ApplicationMetadata appMetadata = ApplicationLibrary.LoadAndSaveMetaData(TitleIdString); - Title = appMetadata.Title ?? TitleIdString; + Gommon.Optional appMetadata = ApplicationLibrary.LoadAndSaveMetaData(TitleIdString); + if (appMetadata != null) + { + Title = appMetadata.Value.Title ?? TitleIdString; + } + else + { + Title = ""; + } } Task.Run(() => diff --git a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs index e5f085e0f..68559d6c1 100644 --- a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs @@ -574,7 +574,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input if (Devices.Any(controller => controller.Name == name)) { controllerNumber++; - name = GetGamepadName(gamepad, controllerNumber); + name = GetUniqueGamepadName(gamepad, ref controllerNumber); } return name; @@ -789,6 +789,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input StrongRumble = 1f, WeakRumble = 1f, EnableRumble = false, + UseHDRumble = true }, }; } diff --git a/src/Ryujinx/UI/ViewModels/Input/RumbleInputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/RumbleInputViewModel.cs index e2323f567..74e0cd289 100644 --- a/src/Ryujinx/UI/ViewModels/Input/RumbleInputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Input/RumbleInputViewModel.cs @@ -9,5 +9,8 @@ namespace Ryujinx.Ava.UI.ViewModels.Input [ObservableProperty] public partial float WeakRumble { get; set; } + + [ObservableProperty] + public partial bool EnableHDRumble { get; set; } } } diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index ae84a15a2..e488495d6 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -5,6 +5,7 @@ using Avalonia.Input; using Avalonia.Media; using Avalonia.Platform.Storage; using Avalonia.Threading; +using System.Runtime.Versioning; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DynamicData; @@ -656,10 +657,19 @@ namespace Ryujinx.Ava.UI.ViewModels get => ConfigurationState.Instance.UI.ShowConsole; set { + bool restartRequired = value && !ConsoleHelper.HasConsoleWindow; + ConfigurationState.Instance.UI.ShowConsole.Value = value; ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + if (restartRequired) + { + NotificationHelper.ShowInformation( + LocaleManager.Instance[LocaleKeys.SettingsAppRequiredRestartMessage], + LocaleManager.Instance[LocaleKeys.SettingsShowConsoleRestartMessage]); + } + OnPropertyChanged(); } } @@ -1760,11 +1770,6 @@ namespace Ryujinx.Ava.UI.ViewModels Logger.RestartTime(); - SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path, - ConfigurationState.Instance.System.Language, application.Id); - - PrepareLoadScreen(); - RendererHostControl = new RendererHost(); AppHost = new AppHost( @@ -1778,18 +1783,34 @@ namespace Ryujinx.Ava.UI.ViewModels UserChannelPersistence, this, TopLevel); + + CancellationTokenSource cts = new CancellationTokenSource(); - if (!await AppHost.LoadGuestApplication(customNacpData)) + try { + await AppHost.LoadGuestApplication(cts, customNacpData); + } + catch (OperationCanceledException exception) + { + Logger.Info?.Print(LogClass.Application, + "LoadGuestApplication was interrupted !!! " + exception.Message); AppHost.DisposeContext(); AppHost = null; - return; } - + finally + { + cts.Dispose(); + } + CanUpdate = false; application.Name ??= AppHost.Device.Processes.ActiveApplication.Name; + + SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path, + ConfigurationState.Instance.System.Language, application.Id); + + PrepareLoadScreen(); LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, application.Name); @@ -1811,9 +1832,9 @@ namespace Ryujinx.Ava.UI.ViewModels RendererHostControl.Focus(); }); - public static void UpdateGameMetadata(string titleId, TimeSpan playTime) + public static void UpdateGameMetadata(string titleId, TimeSpan playTime) => ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame(playTime)); - + public void RefreshFirmwareStatus() { SystemVersion version = null; @@ -1994,7 +2015,7 @@ namespace Ryujinx.Ava.UI.ViewModels LastFullscreenToggle = Environment.TickCount64; - if (WindowState is not WindowState.Normal) + if (WindowState is WindowState.FullScreen) { WindowState = WindowState.Normal; Window.TitleBar.ExtendsContentIntoTitleBar = !ConfigurationState.Instance.ShowOldUI; @@ -2003,21 +2024,74 @@ namespace Ryujinx.Ava.UI.ViewModels { ShowMenuAndStatusBar = true; } + + if (OperatingSystem.IsWindows()) + { + RestoreWindowFromFullscreen(); + } } else { - WindowState = WindowState.FullScreen; Window.TitleBar.ExtendsContentIntoTitleBar = true; if (IsGameRunning) { ShowMenuAndStatusBar = false; } + + if (OperatingSystem.IsWindows()) + { + MakeWindowFullscreen(); + } + else + { + WindowState = WindowState.FullScreen; + } } IsFullScreen = WindowState is WindowState.FullScreen; } + private nint _savedWindowStyle; + + [SupportedOSPlatform("windows")] + private void MakeWindowFullscreen() + { + nint hwnd = Window.TryGetPlatformHandle()?.Handle ?? nint.Zero; + if (hwnd == nint.Zero) return; + + // Save current style and placement + _savedWindowStyle = Win32NativeInterop.GetWindowLongPtrW(hwnd, Win32NativeInterop.GWL_STYLE); + + // Remove window chrome: WS_OVERLAPPEDWINDOW -> WS_POPUP | WS_VISIBLE + Win32NativeInterop.SetWindowLongPtrW(hwnd, Win32NativeInterop.GWL_STYLE, + unchecked((nint)(Win32NativeInterop.WS_POPUP | Win32NativeInterop.WS_VISIBLE))); + + Avalonia.Platform.Screen? screen = Window.Screens.ScreenFromVisual(Window); + int w = screen?.Bounds.Width ?? 0; + int h = screen?.Bounds.Height ?? 0; + + Win32NativeInterop.SetWindowPos(hwnd, nint.Zero, 0, 0, w, h, + Win32NativeInterop.SWP_NOZORDER | Win32NativeInterop.SWP_NOACTIVATE | Win32NativeInterop.SWP_FRAMECHANGED); + + WindowState = WindowState.FullScreen; + } + + [SupportedOSPlatform("windows")] + private void RestoreWindowFromFullscreen() + { + nint hwnd = Window.TryGetPlatformHandle()?.Handle ?? nint.Zero; + if (hwnd == nint.Zero) return; + + // Restore original window style + Win32NativeInterop.SetWindowLongPtrW(hwnd, Win32NativeInterop.GWL_STYLE, _savedWindowStyle); + + Win32NativeInterop.SetWindowPos(hwnd, nint.Zero, 0, 0, 0, 0, + Win32NativeInterop.SWP_NOZORDER | Win32NativeInterop.SWP_NOACTIVATE | + Win32NativeInterop.SWP_FRAMECHANGED | Win32NativeInterop.SWP_NOMOVE | Win32NativeInterop.SWP_NOSIZE); + + } + public static void SaveConfig() { ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index abb284960..6904d4ebc 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -273,6 +273,7 @@ namespace Ryujinx.Ava.UI.ViewModels public bool EnableTrace { get; set; } public bool EnableGuest { get; set; } public bool EnableFsAccessLog { get; set; } + public bool EnableNetLog { get; set; } public bool EnableAvaloniaLog { get; set; } public bool EnableDebug { get; set; } public bool IsOpenAlEnabled { get; set; } @@ -725,6 +726,7 @@ namespace Ryujinx.Ava.UI.ViewModels EnableGuest = config.Logger.EnableGuest; EnableDebug = config.Logger.EnableDebug; EnableFsAccessLog = config.Logger.EnableFsAccessLog; + EnableNetLog = config.Logger.EnableNetLog; EnableAvaloniaLog = config.Logger.EnableAvaloniaLog; FsGlobalAccessLogMode = config.System.FsGlobalAccessLogMode; OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value; @@ -848,6 +850,7 @@ namespace Ryujinx.Ava.UI.ViewModels config.Logger.EnableGuest.Value = EnableGuest; config.Logger.EnableDebug.Value = EnableDebug; config.Logger.EnableFsAccessLog.Value = EnableFsAccessLog; + config.Logger.EnableNetLog.Value = EnableNetLog; config.Logger.EnableAvaloniaLog.Value = EnableAvaloniaLog; config.System.FsGlobalAccessLogMode.Value = FsGlobalAccessLogMode; config.Logger.GraphicsDebugLevel.Value = (GraphicsDebugLevel)OpenglDebugLevel; diff --git a/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml b/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml index 98489aab0..49eb1a717 100644 --- a/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml +++ b/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml @@ -53,6 +53,15 @@ Margin="5,0" Text="{Binding WeakRumble, StringFormat=\{0:0.00\}}" /> + + + diff --git a/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml.cs b/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml.cs index 347d011d5..655bdf591 100644 --- a/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml.cs +++ b/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml.cs @@ -22,6 +22,7 @@ namespace Ryujinx.Ava.UI.Views.Input { StrongRumble = config.StrongRumble, WeakRumble = config.WeakRumble, + EnableHDRumble = config.UseHDRumble }; InitializeComponent(); @@ -45,6 +46,7 @@ namespace Ryujinx.Ava.UI.Views.Input GamepadInputConfig config = viewModel.Config; config.StrongRumble = content.ViewModel.StrongRumble; config.WeakRumble = content.ViewModel.WeakRumble; + config.UseHDRumble = content.ViewModel.EnableHDRumble; }; await contentDialog.ShowAsync(); diff --git a/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml index fa3b4e866..325c4f599 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml @@ -70,6 +70,10 @@ ToolTip.Tip="{ext:Locale FileAccessLogTooltip}"> + + +