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
[](https://update.ryujinx.app/latest/stable)
[](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}">
+
+
+
|