Compare commits

...

15 Commits

Author SHA1 Message Date
Mabel
81468c1d25 Discord Rich Presence: Tomodachi Life LTD and Animal Crossing New Horizons (#103)
Adds Discord Rich Presence for Tomodachi Life: Living the Dream, Tomodachi Life: Living the Dream - Welcome Version, and Animal Crossing: New Horizons

Tomodachi Life Rich Presence uses your total Mii count, and your island level
![image](/attachments/46c20fba-f092-4f8c-af8d-c71340d94e78)
Animal Crossing New Horizons Rich Presence uses your island name
![image](/attachments/f9dfbeaa-86a3-4989-b74a-d65b1d8e6260)

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/103
2026-05-20 13:38:57 +00:00
Max
004a12005e UI: Included more launch checks (#4)
- Added Onedrive folder check for Windows.
- Added iCloud and Downloads folder checks for macOS.
- Added sudo checks for macOS/Linux.
- Added dialogue prompts for macOS/Linux.
- Added unofficial build warning for detected Flatpak install.

These checks have console fallbacks in case the GUI decides it doesn't want to work that day.

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/4
2026-05-20 13:37:05 +00:00
Mabel
a3eda287b5 Fix Clipboard Copy Operation Crash (#108)
Fixes a COM exception crash related to clipboard copy events from changes in Avalonia 11.3

Solves [Ryubing/Issues#294](https://github.com/Ryubing/Issues/issues/294) caused by Avalonia 11.3's changes, see [AvaloniaUI/Avalonia#20007](https://github.com/AvaloniaUI/Avalonia/issues/20007) for more information

I've only tested this on Windows, no idea if this has issues in MacOS or Linux, or if it's even a problem there at all.

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/108
2026-05-20 13:22:56 +00:00
Max
ce340e5d0b [HID] Fixed HD Rumble latency (#104)
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/104
2026-05-20 13:00:09 +00:00
Max
d94b759e89 Vulkan Package Update - Part 1 (#16)
As described. Should hopefully bring speed-ups for macOS devices (thanks Ori!) and general improvements across the board for the vulkan backend (maybe).
This will be followed up with a PR from @KeatonTheBot.

Co-authored-by: V380-Ori <infiniteloop0finsanity@gmail.com>
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/16
2026-05-19 20:23:06 +00:00
AsperTheDog
b7772462f1 Fix crash on Mac and Android caused by a buffer validation error fix in #92 (#105)
This PR fixes a bug introduced in #92. In that PR, the problematic commit did its work directly on the updater's own arrays, calling Auto.Get() on each entry as it went, and nulling entries out along the way. The problem is that Get() can call back into Commit (via ClearMirrors -> Rebind), and when it did, that reentrant Commit would read from the same arrays the outer call was still in the middle of processing, hit one of the entries the outer had already nulled, and throw an NullReferenceException. The fix is to have Commit start by copying everything it needs into local variables and resetting _count to zero, so a reentrant call sees a clean updater and operates on its own data. The outer call then writes its snapshot back into the native arrays just before recording the Vulkan bind.

Co-authored-by: AsperTheDog <guillerman0000@gmail.com>
Co-authored-by: Max <randomgirlisweird@gmail.com>
Co-authored-by: Renovate Bot <renovatebot@ryujinx.app>
Co-authored-by: Babib3l <gab.chevanne@gmail.com>
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/105
2026-05-18 16:34:04 +00:00
Babib3l
3473044c6e New French Translations (#101)
French translations + capital letters on spanish translations

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/101
2026-05-16 13:28:02 +00:00
Babib3l
e756ad4556 Fix ProcessLoader stale PID validation against kernel process table (#102)
This PR adresses the following issue : `ProcessLoader.ActiveApplication` could return invalid results when `_latestPid` pointed to a process that no longer existed in the kernel's process table. The original exception path was commented out and bypassed (by sh0inx?) with `GetValueOrDefault` to prevent UI lockups, but this only resolved the symptoms without fixing the root cause.

This was due to sevral factors :
- `_latestPid` was never reset or validated against the actual process state
- ProcessLoader maintained its own `_processesByPid` dictionary separate from the kernel's `KernelContext.Processes`
- No cleanups happened when processes exited or were terminated
- ProcessLoader state could drift out of sync with the kernel process table

**Solution/Fixes**
- Validate` _latestPid` against the kernel process table before returning `ActiveApplication`
- Check process state (Exited/Exiting) and automatically clear stale references
- Add thread-safe cleanup methods (`ClearProcess`, `ClearAllProcesses`)
- Integrate `ClearAllProcesses` into Switch.Dispose for proper shutdown cleanup
- Add warning logs when stale PID is detected and cleared for debugging

**Code Changes**:
- `ProcessLoader.cs`: Add `_pidLock`, update `ActiveApplication` with validation, add cleanup methods
- `Switch.cs`: Call `Processes.ClearAllProcesses()` in Dispose()

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/102
2026-05-16 01:11:06 +00:00
AsperTheDog
58bd19a2f3 Fix Vulkan validation errors (#92)
This PR fixes several validation errors caused by invalid Vulkan usage. These validation errors often end up invoking Undefined Behavior on the driver side, which can lead to artifacts or crashes which are driver specific and otherwise incredibly hard to track. I don't think it should have any impact on performance, but it would be good to test it with as many games as possible (maybe a bug in a game was fixed?).

Each commit fixes an error. I added to each a description with the validation error that was fixed and a small explanation on what was causing it and how I fixed it.

Co-authored-by: AsperTheDog <guillerman0000@gmail.com>
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/92
2026-05-15 15:36:14 +00:00
AsperTheDog
48888bd014 Change non-unform shader extension to be more conservative (#99)
The previous fix for Tomodachi Life (#91) included the extension to all shaders, independently on if it was needed or not. This PR fixes that by lazily adding the extension only when it is actually needed.

This change should not be noticed by anyone, but it avoids having to modify shaders that do not perform any type of dynamic indexing, which apparently is something some modders care about.

Co-authored-by: AsperTheDog <guillerman0000@gmail.com>
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/99
2026-05-15 15:19:12 +00:00
Renovate Bot
d307afc454 Update dependency Sep to 0.14.1 (#98)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [Sep](https://github.com/nietras/Sep) | `0.13.0` → `0.14.1` | ![age](https://developer.mend.io/api/mc/badges/age/nuget/Sep/0.14.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Sep/0.13.0/0.14.1?slim=true) |

---

### Release Notes

<details>
<summary>nietras/Sep (Sep)</summary>

### [`v0.14.1`](https://github.com/nietras/Sep/releases/tag/v0.14.1): 0.14.1

#### What's Changed

- Improve SepReaderOptions.Unescape/.Trim comments by [@&#8203;nietras](https://github.com/nietras) in [#&#8203;542](https://github.com/nietras/Sep/pull/542)
- Improve SepReader/WriterExtensions.Strict() comments by [@&#8203;nietras](https://github.com/nietras) in [#&#8203;543](https://github.com/nietras/Sep/pull/543)

**Full Changelog**: <https://github.com/nietras/Sep/compare/v0.14.0...v0.14.1>

### [`v0.14.0`](https://github.com/nietras/Sep/releases/tag/v0.14.0): 0.14.0

#### What's Changed

- Add `leaveOpen` overloads for SepReaderOptions.From\* via SepTextReaderDisposers by [@&#8203;Copilot](https://github.com/Copilot) in [#&#8203;530](https://github.com/nietras/Sep/pull/530)
- Bump MSTest from 4.2.1 to 4.2.2 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;533](https://github.com/nietras/Sep/pull/533)
- Bump github/codeql-action from 4.35.2 to 4.35.3 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;532](https://github.com/nietras/Sep/pull/532)
- Bump step-security/harden-runner from 2.19.0 to 2.19.1 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;531](https://github.com/nietras/Sep/pull/531)
- Confirm statuses:write is the correct minimum permission for super-linter (not checks:write) by [@&#8203;Copilot](https://github.com/Copilot) in [#&#8203;540](https://github.com/nietras/Sep/pull/540)

**Full Changelog**: <https://github.com/nietras/Sep/compare/v0.13.0...v0.14.0>

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xNzguMCIsInVwZGF0ZWRJblZlciI6IjQzLjE3OC4wIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/98
2026-05-15 15:14:23 +00:00
Renovate Bot
134453e62b Update dependency SharpCompress to 0.48.1 (#97)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [SharpCompress](https://github.com/adamhathcock/sharpcompress) | `0.48.0` → `0.48.1` | ![age](https://developer.mend.io/api/mc/badges/age/nuget/SharpCompress/0.48.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/SharpCompress/0.48.0/0.48.1?slim=true) |

---

### Release Notes

<details>
<summary>adamhathcock/sharpcompress (SharpCompress)</summary>

### [`v0.48.1`](https://github.com/adamhathcock/sharpcompress/releases/tag/0.48.1): - GZip writing fix

[Compare Source](https://github.com/adamhathcock/sharpcompress/compare/0.48.0...0.48.1)

#### What's Changed

- release test cleanup by [@&#8203;adamhathcock](https://github.com/adamhathcock) in [#&#8203;1322](https://github.com/adamhathcock/sharpcompress/pull/1322)
- Fix GZip write async by [@&#8203;adamhathcock](https://github.com/adamhathcock) in [#&#8203;1320](https://github.com/adamhathcock/sharpcompress/pull/1320)

**Full Changelog**: <https://github.com/adamhathcock/sharpcompress/compare/0.48.0...0.48.1>

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xNzguMCIsInVwZGF0ZWRJblZlciI6IjQzLjE3OC4wIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/97
2026-05-15 15:13:49 +00:00
Max
298b6c3959 [HLE] Stub ILibrarySelfAccessor:ExitAndReturn (10) (#96)
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/96
2026-05-15 15:13:23 +00:00
shinyoyo
c96433eb13 Updated Simplified Chinese translation (#95)
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/95
2026-05-14 14:00:16 +00:00
AsperTheDog
c0078088dd Add shader non-uniform indexing support (#91)
This PR marks ALL texture indexes as nonuniform to fix an issue with the paths in Tomodachi Life: Living the Dream on AMD cards. It should have a negligible impact on performance (and it should not have an impact at all on NVIDIA cards!)

It's caused by what is called 'implicit non-uniform sampler array indexing'. The idea is basically that some GPUs optimize texture lookups from indexed texture arrays, by assuming that you are never going to index different textures within a single workgroup. What this causes is that visual glitch where a subgroup is tasked with rendering a block of the screen, and in the boundaries some cores are indexing the wrong texture.

Co-authored-by: AsperTheDog <guillerman0000@gmail.com>
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/91
2026-05-14 08:59:10 +00:00
51 changed files with 832 additions and 160 deletions

View File

@@ -8,7 +8,7 @@
<PackageVersion Include="Avalonia.Desktop" Version="11.3.15" /> <PackageVersion Include="Avalonia.Desktop" Version="11.3.15" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.15" /> <PackageVersion Include="Avalonia.Diagnostics" Version="11.3.15" />
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.3.15" /> <PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.3.15" />
<PackageVersion Include="SharpCompress" Version="0.48.0" /> <PackageVersion Include="SharpCompress" Version="0.48.1" />
<PackageVersion Include="Svg.Controls.Avalonia" Version="11.3.9.5" /> <PackageVersion Include="Svg.Controls.Avalonia" Version="11.3.9.5" />
<PackageVersion Include="Svg.Controls.Skia.Avalonia" Version="11.3.9.5" /> <PackageVersion Include="Svg.Controls.Skia.Avalonia" Version="11.3.9.5" />
<PackageVersion Include="Microsoft.Build.Framework" Version="17.11.4" /> <PackageVersion Include="Microsoft.Build.Framework" Version="17.11.4" />
@@ -45,17 +45,18 @@
<!--<PackageVersion Include="Ryujinx.Audio.OpenAL.Dependencies" Version="1.21.0.1" />--> <!--<PackageVersion Include="Ryujinx.Audio.OpenAL.Dependencies" Version="1.21.0.1" />-->
<PackageVersion Include="Ryujinx.Audio.OpenAL" Version="1.25.1" /> <PackageVersion Include="Ryujinx.Audio.OpenAL" Version="1.25.1" />
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies.AllArch" Version="6.1.4-build6" /> <PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies.AllArch" Version="6.1.4-build6" />
<PackageVersion Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Version="1.2.0" /> <PackageVersion Include="Ryujinx.Graphics.Vulkan.MoltenVK" Version="1.4.2-ryujinx.3" />
<PackageVersion Include="Ryujinx.LibHac" Version="0.21.0-alpha.133" /> <PackageVersion Include="Ryujinx.LibHac" Version="0.21.0-alpha.133" />
<PackageVersion Include="Ryujinx.UpdateClient" Version="2.0.6" /> <PackageVersion Include="Ryujinx.UpdateClient" Version="2.0.6" />
<PackageVersion Include="Ryujinx.Systems.Update.Common" Version="2.0.6" /> <PackageVersion Include="Ryujinx.Systems.Update.Common" Version="2.0.6" />
<PackageVersion Include="Gommon" Version="2.8.1.2" /> <PackageVersion Include="Gommon" Version="2.8.1.2" />
<PackageVersion Include="securifybv.ShellLink" Version="0.1.0" /> <PackageVersion Include="securifybv.ShellLink" Version="0.1.0" />
<PackageVersion Include="Sep" Version="0.13.0" /> <PackageVersion Include="Sep" Version="0.14.1" />
<PackageVersion Include="shaderc.net" Version="0.1.0" /> <PackageVersion Include="shaderc.net" Version="0.1.0" />
<PackageVersion Include="Silk.NET.Vulkan" Version="2.22.0" /> <PackageVersion Include="SharpZipLib" Version="1.4.2" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.22.0" /> <PackageVersion Include="Silk.NET.Vulkan" Version="2.23.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.22.0" /> <PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.23.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" />
<PackageVersion Include="SkiaSharp" Version="2.88.9" /> <PackageVersion Include="SkiaSharp" Version="2.88.9" />
<PackageVersion Include="SkiaSharp.NativeAssets.Win32" Version="2.88.9" /> <PackageVersion Include="SkiaSharp.NativeAssets.Win32" Version="2.88.9" />
<PackageVersion Include="SkiaSharp.NativeAssets.macOS" Version="2.88.9" /> <PackageVersion Include="SkiaSharp.NativeAssets.macOS" Version="2.88.9" />

View File

@@ -596,7 +596,7 @@
"th_TH": "", "th_TH": "",
"tr_TR": "", "tr_TR": "",
"uk_UA": "", "uk_UA": "",
"zh_CN": "", "zh_CN": "重启模拟",
"zh_TW": "重新啟動模擬" "zh_TW": "重新啟動模擬"
} }
}, },
@@ -6107,8 +6107,8 @@
"de_DE": "", "de_DE": "",
"el_GR": "", "el_GR": "",
"en_US": "Enable Net Logs", "en_US": "Enable Net Logs",
"es_ES": "Habilitar registros de red.", "es_ES": "Habilitar Registros de Red.",
"fr_FR": "", "fr_FR": "Activer les Journaux Réseau.",
"he_IL": "", "he_IL": "",
"it_IT": "", "it_IT": "",
"ja_JP": "", "ja_JP": "",
@@ -6121,7 +6121,7 @@
"th_TH": "", "th_TH": "",
"tr_TR": "", "tr_TR": "",
"uk_UA": "", "uk_UA": "",
"zh_CN": "", "zh_CN": "开启网络日志",
"zh_TW": "" "zh_TW": ""
} }
}, },
@@ -11371,7 +11371,7 @@
"th_TH": "", "th_TH": "",
"tr_TR": "", "tr_TR": "",
"uk_UA": "", "uk_UA": "",
"zh_CN": "", "zh_CN": "保存",
"zh_TW": "儲存" "zh_TW": "儲存"
} }
}, },
@@ -12250,6 +12250,56 @@
"zh_TW": "弱震動調節:" "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", "ID": "DialogMessageSaveNotAvailableMessage",
"Translations": { "Translations": {
@@ -17108,7 +17158,7 @@
"el_GR": "", "el_GR": "",
"en_US": "Prints network log messages in the console.", "en_US": "Prints network log messages in the console.",
"es_ES": "Imprimir registros de red en la consola.", "es_ES": "Imprimir registros de red en la consola.",
"fr_FR": "", "fr_FR": "Affiche les journaux réseau dans la console.",
"he_IL": "", "he_IL": "",
"it_IT": "", "it_IT": "",
"ja_JP": "", "ja_JP": "",
@@ -17121,7 +17171,7 @@
"th_TH": "", "th_TH": "",
"tr_TR": "", "tr_TR": "",
"uk_UA": "", "uk_UA": "",
"zh_CN": "", "zh_CN": "在控制台中显示网络日志",
"zh_TW": "" "zh_TW": ""
} }
}, },
@@ -21496,7 +21546,7 @@
"th_TH": "", "th_TH": "",
"tr_TR": "", "tr_TR": "",
"uk_UA": "", "uk_UA": "",
"zh_CN": "", "zh_CN": "控制台将会在下次启动 Ryujinx 时可用。",
"zh_TW": "" "zh_TW": ""
} }
}, },

View File

@@ -47,14 +47,12 @@ def get_new_name(
input_component = str(input_dylib_path).replace(str(input_directory), "")[1:] input_component = str(input_dylib_path).replace(str(input_directory), "")[1:]
return Path(os.path.join(output_directory, input_component)) return Path(os.path.join(output_directory, input_component))
def get_archs(dylib_path: Path) -> list[str]:
def is_fat_file(dylib_path: Path) -> str: res = subprocess.check_output([LIPO, "-info", str(dylib_path)]).decode("utf-8")
res = subprocess.check_output([LIPO, "-info", str(dylib_path.absolute())]).decode( if res.startswith("Non-fat file"):
"utf-8" return [res.split(":")[-1].strip()]
) else:
return res.split("are:")[-1].strip().split()
return not res.split("\n")[0].startswith("Non-fat file")
def construct_universal_dylib( def construct_universal_dylib(
arm64_input_dylib_path: Path, x86_64_input_dylib_path: Path, output_dylib_path: Path 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 os.path.basename(arm64_input_dylib_path.resolve()), output_dylib_path
) )
else: else:
if is_fat_file(arm64_input_dylib_path) or not x86_64_input_dylib_path.exists(): arm64_archs = get_archs(arm64_input_dylib_path)
with open(output_dylib_path, "wb") as dst: x86_64_archs = get_archs(x86_64_input_dylib_path) if x86_64_input_dylib_path.exists() else []
with open(arm64_input_dylib_path, "rb") as src:
dst.write(src.read()) if "arm64" in arm64_archs and "x86_64" in arm64_archs:
else: shutil.copy2(arm64_input_dylib_path, output_dylib_path)
elif x86_64_archs:
subprocess.check_call( subprocess.check_call(
[ [
LIPO, LIPO,

View File

@@ -16,5 +16,10 @@ namespace Ryujinx.Common.Configuration.Hid.Controller
/// Enable Rumble /// Enable Rumble
/// </summary> /// </summary>
public bool EnableRumble { get; set; } public bool EnableRumble { get; set; }
/// <summary>
/// Enable HD Rumble support
/// </summary
public bool UseHDRumble { get; set; }
} }
} }

View File

@@ -42,6 +42,7 @@ namespace Ryujinx.Graphics.GAL
public readonly bool SupportsShaderBallot; public readonly bool SupportsShaderBallot;
public readonly bool SupportsShaderBarrierDivergence; public readonly bool SupportsShaderBarrierDivergence;
public readonly bool SupportsShaderFloat64; public readonly bool SupportsShaderFloat64;
public readonly bool SupportsShaderNonUniformIndexing;
public readonly bool SupportsTextureGatherOffsets; public readonly bool SupportsTextureGatherOffsets;
public readonly bool SupportsTextureShadowLod; public readonly bool SupportsTextureShadowLod;
public readonly bool SupportsVertexStoreAndAtomics; public readonly bool SupportsVertexStoreAndAtomics;
@@ -110,6 +111,7 @@ namespace Ryujinx.Graphics.GAL
bool supportsShaderBallot, bool supportsShaderBallot,
bool supportsShaderBarrierDivergence, bool supportsShaderBarrierDivergence,
bool supportsShaderFloat64, bool supportsShaderFloat64,
bool supportsShaderNonUniformIndexing,
bool supportsTextureGatherOffsets, bool supportsTextureGatherOffsets,
bool supportsTextureShadowLod, bool supportsTextureShadowLod,
bool supportsVertexStoreAndAtomics, bool supportsVertexStoreAndAtomics,
@@ -172,6 +174,7 @@ namespace Ryujinx.Graphics.GAL
SupportsShaderBallot = supportsShaderBallot; SupportsShaderBallot = supportsShaderBallot;
SupportsShaderBarrierDivergence = supportsShaderBarrierDivergence; SupportsShaderBarrierDivergence = supportsShaderBarrierDivergence;
SupportsShaderFloat64 = supportsShaderFloat64; SupportsShaderFloat64 = supportsShaderFloat64;
SupportsShaderNonUniformIndexing = supportsShaderNonUniformIndexing;
SupportsTextureGatherOffsets = supportsTextureGatherOffsets; SupportsTextureGatherOffsets = supportsTextureGatherOffsets;
SupportsTextureShadowLod = supportsTextureShadowLod; SupportsTextureShadowLod = supportsTextureShadowLod;
SupportsVertexStoreAndAtomics = supportsVertexStoreAndAtomics; SupportsVertexStoreAndAtomics = supportsVertexStoreAndAtomics;

View File

@@ -22,7 +22,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
private const ushort FileFormatVersionMajor = 1; private const ushort FileFormatVersionMajor = 1;
private const ushort FileFormatVersionMinor = 2; private const ushort FileFormatVersionMinor = 2;
private const uint FileFormatVersionPacked = ((uint)FileFormatVersionMajor << 16) | FileFormatVersionMinor; 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 SharedTocFileName = "shared.toc";
private const string SharedDataFileName = "shared.data"; private const string SharedDataFileName = "shared.data";

View File

@@ -231,6 +231,8 @@ namespace Ryujinx.Graphics.Gpu.Shader
public bool QueryHostSupportsShaderFloat64() => _context.Capabilities.SupportsShaderFloat64; public bool QueryHostSupportsShaderFloat64() => _context.Capabilities.SupportsShaderFloat64;
public bool QueryHostSupportsShaderNonUniformIndexing() => _context.Capabilities.SupportsShaderNonUniformIndexing;
public bool QueryHostSupportsSnormBufferTextureFormat() => _context.Capabilities.SupportsSnormBufferTextureFormat; public bool QueryHostSupportsSnormBufferTextureFormat() => _context.Capabilities.SupportsSnormBufferTextureFormat;
public bool QueryHostSupportsTextureGatherOffsets() => _context.Capabilities.SupportsTextureGatherOffsets; public bool QueryHostSupportsTextureGatherOffsets() => _context.Capabilities.SupportsTextureGatherOffsets;

View File

@@ -184,6 +184,7 @@ namespace Ryujinx.Graphics.OpenGL
supportsShaderBallot: HwCapabilities.SupportsShaderBallot, supportsShaderBallot: HwCapabilities.SupportsShaderBallot,
supportsShaderBarrierDivergence: !(intelWindows || intelUnix), supportsShaderBarrierDivergence: !(intelWindows || intelUnix),
supportsShaderFloat64: true, supportsShaderFloat64: true,
supportsShaderNonUniformIndexing: false,
supportsTextureGatherOffsets: true, supportsTextureGatherOffsets: true,
supportsTextureShadowLod: HwCapabilities.SupportsTextureShadowLod, supportsTextureShadowLod: HwCapabilities.SupportsTextureShadowLod,
supportsVertexStoreAndAtomics: true, supportsVertexStoreAndAtomics: true,

View File

@@ -82,6 +82,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
public bool IsMainFunction { get; private set; } public bool IsMainFunction { get; private set; }
public bool MayHaveReturned { get; set; } public bool MayHaveReturned { get; set; }
public bool WasNonUniformAccessDeclared { get; set; }
public CodeGenContext( public CodeGenContext(
StructuredProgramInfo info, StructuredProgramInfo info,
@@ -89,6 +90,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
GeneratorPool<Instruction> instPool, GeneratorPool<Instruction> instPool,
GeneratorPool<LiteralInteger> integerPool) : base(SpirvVersionPacked, instPool, integerPool) GeneratorPool<LiteralInteger> integerPool) : base(SpirvVersionPacked, instPool, integerPool)
{ {
WasNonUniformAccessDeclared = false;
Info = info; Info = info;
AttributeUsage = parameters.AttributeUsage; AttributeUsage = parameters.AttributeUsage;
Definitions = parameters.Definitions; Definitions = parameters.Definitions;

View File

@@ -587,6 +587,23 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
return OperationResult.Invalid; 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) private static OperationResult GenerateImageAtomic(CodeGenContext context, AstOperation operation)
{ {
AstTextureOperation texOp = (AstTextureOperation)operation; AstTextureOperation texOp = (AstTextureOperation)operation;
@@ -613,6 +630,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
SpvInstruction textureIndex = Src(AggregateType.S32); SpvInstruction textureIndex = Src(AggregateType.S32);
image = context.AccessChain(imagePointerType, image, textureIndex); image = context.AccessChain(imagePointerType, image, textureIndex);
MarkNonUniform(context, image);
} }
int coordsCount = texOp.Type.Dimensions; int coordsCount = texOp.Type.Dimensions;
@@ -683,15 +701,21 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
ImageDeclaration declaration = context.Images[texOp.GetTextureSetAndBinding()]; ImageDeclaration declaration = context.Images[texOp.GetTextureSetAndBinding()];
SpvInstruction image = declaration.Image; SpvInstruction image = declaration.Image;
bool isIndexed = declaration.IsIndexed;
if (declaration.IsIndexed) if (isIndexed)
{ {
SpvInstruction textureIndex = Src(AggregateType.S32); SpvInstruction textureIndex = Src(AggregateType.S32);
image = context.AccessChain(declaration.ImagePointerType, image, textureIndex); image = context.AccessChain(declaration.ImagePointerType, image, textureIndex);
MarkNonUniform(context, image);
} }
image = context.Load(declaration.ImageType, image); image = context.Load(declaration.ImageType, image);
if (isIndexed)
{
MarkNonUniform(context, image);
}
int coordsCount = texOp.Type.Dimensions; int coordsCount = texOp.Type.Dimensions;
@@ -740,15 +764,21 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
ImageDeclaration declaration = context.Images[texOp.GetTextureSetAndBinding()]; ImageDeclaration declaration = context.Images[texOp.GetTextureSetAndBinding()];
SpvInstruction image = declaration.Image; SpvInstruction image = declaration.Image;
bool isIndexed = declaration.IsIndexed;
if (declaration.IsIndexed) if (isIndexed)
{ {
SpvInstruction textureIndex = Src(AggregateType.S32); SpvInstruction textureIndex = Src(AggregateType.S32);
image = context.AccessChain(declaration.ImagePointerType, image, textureIndex); image = context.AccessChain(declaration.ImagePointerType, image, textureIndex);
MarkNonUniform(context, image);
} }
image = context.Load(declaration.ImageType, image); image = context.Load(declaration.ImageType, image);
if (isIndexed)
{
MarkNonUniform(context, image);
}
int coordsCount = texOp.Type.Dimensions; 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) private static SpvInstruction GenerateSampledImageLoad(CodeGenContext context, AstTextureOperation texOp, SamplerDeclaration declaration, ref int srcIndex)
{ {
SpvInstruction image = declaration.Image; SpvInstruction image = declaration.Image;
bool imageIndexed = declaration.IsIndexed;
if (declaration.IsIndexed) if (imageIndexed)
{ {
SpvInstruction textureIndex = context.Get(AggregateType.S32, texOp.GetSource(srcIndex++)); SpvInstruction textureIndex = context.Get(AggregateType.S32, texOp.GetSource(srcIndex++));
image = context.AccessChain(declaration.SampledImagePointerType, image, textureIndex); image = context.AccessChain(declaration.SampledImagePointerType, image, textureIndex);
MarkNonUniform(context, image);
} }
if (texOp.IsSeparate) if (texOp.IsSeparate)
{ {
image = context.Load(declaration.ImageType, image); image = context.Load(declaration.ImageType, image);
if (imageIndexed)
{
MarkNonUniform(context, image);
}
SamplerDeclaration samplerDeclaration = context.Samplers[texOp.GetSamplerSetAndBinding()]; SamplerDeclaration samplerDeclaration = context.Samplers[texOp.GetSamplerSetAndBinding()];
SpvInstruction sampler = samplerDeclaration.Image; SpvInstruction sampler = samplerDeclaration.Image;
bool samplerIndexed = samplerDeclaration.IsIndexed;
if (samplerDeclaration.IsIndexed) if (samplerIndexed)
{ {
SpvInstruction samplerIndex = context.Get(AggregateType.S32, texOp.GetSource(srcIndex++)); SpvInstruction samplerIndex = context.Get(AggregateType.S32, texOp.GetSource(srcIndex++));
sampler = context.AccessChain(samplerDeclaration.SampledImagePointerType, sampler, samplerIndex); sampler = context.AccessChain(samplerDeclaration.SampledImagePointerType, sampler, samplerIndex);
MarkNonUniform(context, sampler);
} }
sampler = context.Load(samplerDeclaration.ImageType, sampler); sampler = context.Load(samplerDeclaration.ImageType, sampler);
if (samplerIndexed)
{
MarkNonUniform(context, sampler);
}
image = context.SampledImage(declaration.SampledImageType, image, sampler); image = context.SampledImage(declaration.SampledImageType, image, sampler);
if (imageIndexed || samplerIndexed)
{
MarkNonUniform(context, image);
}
} }
else else
{ {
image = context.Load(declaration.SampledImageType, image); image = context.Load(declaration.SampledImageType, image);
if (imageIndexed)
{
MarkNonUniform(context, image);
}
} }
return image; return image;

View File

@@ -336,6 +336,10 @@ namespace Ryujinx.Graphics.Shader
{ {
return true; return true;
} }
bool QueryHostSupportsShaderNonUniformIndexing()
{
return false;
}
/// <summary> /// <summary>
/// Queries host GPU support for signed normalized buffer texture formats. /// Queries host GPU support for signed normalized buffer texture formats.

View File

@@ -9,6 +9,7 @@ namespace Ryujinx.Graphics.Shader.Translation
public readonly bool SupportsShaderBallot; public readonly bool SupportsShaderBallot;
public readonly bool SupportsShaderBarrierDivergence; public readonly bool SupportsShaderBarrierDivergence;
public readonly bool SupportsShaderFloat64; public readonly bool SupportsShaderFloat64;
public readonly bool SupportsShaderNonUniformIndexing;
public readonly bool SupportsTextureShadowLod; public readonly bool SupportsTextureShadowLod;
public readonly bool SupportsViewportMask; public readonly bool SupportsViewportMask;
@@ -20,6 +21,7 @@ namespace Ryujinx.Graphics.Shader.Translation
bool supportsShaderBallot, bool supportsShaderBallot,
bool supportsShaderBarrierDivergence, bool supportsShaderBarrierDivergence,
bool supportsShaderFloat64, bool supportsShaderFloat64,
bool supportsShaderNonUniformIndexing,
bool supportsTextureShadowLod, bool supportsTextureShadowLod,
bool supportsViewportMask) bool supportsViewportMask)
{ {
@@ -30,6 +32,7 @@ namespace Ryujinx.Graphics.Shader.Translation
SupportsShaderBallot = supportsShaderBallot; SupportsShaderBallot = supportsShaderBallot;
SupportsShaderBarrierDivergence = supportsShaderBarrierDivergence; SupportsShaderBarrierDivergence = supportsShaderBarrierDivergence;
SupportsShaderFloat64 = supportsShaderFloat64; SupportsShaderFloat64 = supportsShaderFloat64;
SupportsShaderNonUniformIndexing = supportsShaderNonUniformIndexing;
SupportsTextureShadowLod = supportsTextureShadowLod; SupportsTextureShadowLod = supportsTextureShadowLod;
SupportsViewportMask = supportsViewportMask; SupportsViewportMask = supportsViewportMask;
} }

View File

@@ -364,6 +364,7 @@ namespace Ryujinx.Graphics.Shader.Translation
GpuAccessor.QueryHostSupportsShaderBallot(), GpuAccessor.QueryHostSupportsShaderBallot(),
GpuAccessor.QueryHostSupportsShaderBarrierDivergence(), GpuAccessor.QueryHostSupportsShaderBarrierDivergence(),
GpuAccessor.QueryHostSupportsShaderFloat64(), GpuAccessor.QueryHostSupportsShaderFloat64(),
GpuAccessor.QueryHostSupportsShaderNonUniformIndexing(),
GpuAccessor.QueryHostSupportsTextureShadowLod(), GpuAccessor.QueryHostSupportsTextureShadowLod(),
GpuAccessor.QueryHostSupportsViewportMask()); GpuAccessor.QueryHostSupportsViewportMask());

View File

@@ -46,7 +46,14 @@ namespace Ryujinx.Graphics.Vulkan
public static (AccessFlags Access, PipelineStageFlags Stages) GetSubpassAccessSuperset(VulkanRenderer gd) 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; PipelineStageFlags stages = PipelineStageFlags.AllGraphicsBit;
if (gd.TransformFeedbackApi != null) if (gd.TransformFeedbackApi != null)

View File

@@ -42,10 +42,11 @@ namespace Ryujinx.Graphics.Vulkan
ResourceDescriptor descriptor = rdc.Descriptors[descIndex]; ResourceDescriptor descriptor = rdc.Descriptors[descIndex];
ResourceStages stages = descriptor.Stages; 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. // causes invalid resource errors, allow the binding on all active stages as workaround.
// https://github.com/KhronosGroup/MoltenVK/issues/1870
stages = activeStages; stages = activeStages;
} }

View File

@@ -15,6 +15,8 @@ namespace Ryujinx.Graphics.Vulkan
Image dstImage, Image dstImage,
TextureCreateInfo srcInfo, TextureCreateInfo srcInfo,
TextureCreateInfo dstInfo, TextureCreateInfo dstInfo,
TextureCreateInfo srcStorageInfo,
TextureCreateInfo dstStorageInfo,
Extents2D srcRegion, Extents2D srcRegion,
Extents2D dstRegion, Extents2D dstRegion,
int srcLayer, int srcLayer,
@@ -40,6 +42,13 @@ namespace Ryujinx.Graphics.Vulkan
return (xy1, xy2); 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) if (srcAspectFlags == 0)
{ {
srcAspectFlags = srcInfo.Format.ConvertAspectFlags(); srcAspectFlags = srcInfo.Format.ConvertAspectFlags();
@@ -80,6 +89,14 @@ namespace Ryujinx.Graphics.Vulkan
(srcOffsets.Element0, srcOffsets.Element1) = ExtentsToOffset3D(srcRegion, srcInfo.Width, srcInfo.Height, level); (srcOffsets.Element0, srcOffsets.Element1) = ExtentsToOffset3D(srcRegion, srcInfo.Width, srcInfo.Height, level);
(dstOffsets.Element0, dstOffsets.Element1) = ExtentsToOffset3D(dstRegion, dstInfo.Width, dstInfo.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() ImageBlit region = new()
{ {
SrcSubresource = srcSl, SrcSubresource = srcSl,
@@ -121,6 +138,8 @@ namespace Ryujinx.Graphics.Vulkan
Image dstImage, Image dstImage,
TextureCreateInfo srcInfo, TextureCreateInfo srcInfo,
TextureCreateInfo dstInfo, TextureCreateInfo dstInfo,
TextureCreateInfo srcStorageInfo,
TextureCreateInfo dstStorageInfo,
int srcViewLayer, int srcViewLayer,
int dstViewLayer, int dstViewLayer,
int srcViewLevel, int srcViewLevel,
@@ -151,6 +170,8 @@ namespace Ryujinx.Graphics.Vulkan
dstImage, dstImage,
srcInfo, srcInfo,
dstInfo, dstInfo,
srcStorageInfo,
dstStorageInfo,
srcViewLayer, srcViewLayer,
dstViewLayer, dstViewLayer,
srcViewLevel, srcViewLevel,
@@ -186,6 +207,8 @@ namespace Ryujinx.Graphics.Vulkan
Image dstImage, Image dstImage,
TextureCreateInfo srcInfo, TextureCreateInfo srcInfo,
TextureCreateInfo dstInfo, TextureCreateInfo dstInfo,
TextureCreateInfo srcStorageInfo,
TextureCreateInfo dstStorageInfo,
int srcViewLayer, int srcViewLayer,
int dstViewLayer, int dstViewLayer,
int srcViewLevel, int srcViewLevel,
@@ -314,6 +337,14 @@ namespace Ryujinx.Graphics.Vulkan
int copyWidth = sizeInBlocks ? BitUtils.DivRoundUp(width, blockWidth) : width; int copyWidth = sizeInBlocks ? BitUtils.DivRoundUp(width, blockWidth) : width;
int copyHeight = sizeInBlocks ? BitUtils.DivRoundUp(height, blockHeight) : height; 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); Extent3D extent = new((uint)copyWidth, (uint)copyHeight, (uint)srcDepth);
if (srcInfo.Samples > 1 && srcInfo.Samples != dstInfo.Samples) if (srcInfo.Samples > 1 && srcInfo.Samples != dstInfo.Samples)

View File

@@ -67,6 +67,8 @@ namespace Ryujinx.Graphics.Vulkan
public VkFormat VkFormat { get; } public VkFormat VkFormat { get; }
public ImageUsageFlags UsageFlags { get; }
public unsafe TextureStorage( public unsafe TextureStorage(
VulkanRenderer gd, VulkanRenderer gd,
Device device, Device device,
@@ -93,7 +95,8 @@ namespace Ryujinx.Graphics.Vulkan
SampleCountFlags sampleCountFlags = ConvertToSampleCountFlags(gd.Capabilities.SupportedSampleCounts, (uint)info.Samples); 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; ImageCreateFlags flags = ImageCreateFlags.CreateMutableFormatBit | ImageCreateFlags.CreateExtendedUsageBit;
@@ -159,7 +162,7 @@ namespace Ryujinx.Graphics.Vulkan
_imageAuto = new Auto<DisposableImage>(new DisposableImage(_gd.Api, device, _image)); _imageAuto = new Auto<DisposableImage>(new DisposableImage(_gd.Api, device, _image));
InitialTransition(ImageLayout.Preinitialized, ImageLayout.General); InitialTransition(ImageLayout.Undefined, ImageLayout.General);
} }
_slices = new TextureSliceInfo[levels * _depthOrLayers]; _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; ImageUsageFlags usage = DefaultUsageFlags;
@@ -320,7 +323,7 @@ namespace Ryujinx.Graphics.Vulkan
usage |= ImageUsageFlags.ColorAttachmentBit; usage |= ImageUsageFlags.ColorAttachmentBit;
} }
if ((format.IsImageCompatible && isMsImageStorageSupported) || extendedUsage) if (format.IsImageCompatible && isMsImageStorageSupported)
{ {
usage |= ImageUsageFlags.StorageBit; usage |= ImageUsageFlags.StorageBit;
} }

View File

@@ -64,7 +64,7 @@ namespace Ryujinx.Graphics.Vulkan
bool isMsImageStorageSupported = gd.Capabilities.SupportsShaderStorageImageMultisample || !info.Target.IsMultisample; bool isMsImageStorageSupported = gd.Capabilities.SupportsShaderStorageImageMultisample || !info.Target.IsMultisample;
VkFormat format = _gd.FormatCapabilities.ConvertToVkFormat(info.Format, isMsImageStorageSupported); 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 levels = (uint)info.Levels;
uint layers = (uint)info.GetLayers(); uint layers = (uint)info.GetLayers();
@@ -133,6 +133,8 @@ namespace Ryujinx.Graphics.Vulkan
shaderUsage |= ImageUsageFlags.StorageBit; shaderUsage |= ImageUsageFlags.StorageBit;
} }
shaderUsage &= storage.UsageFlags;
_imageView = CreateImageView(componentMapping, subresourceRange, type, shaderUsage); _imageView = CreateImageView(componentMapping, subresourceRange, type, shaderUsage);
// Framebuffer attachments and storage images requires a identity component mapping. // Framebuffer attachments and storage images requires a identity component mapping.
@@ -257,6 +259,8 @@ namespace Ryujinx.Graphics.Vulkan
dstImage, dstImage,
src.Info, src.Info,
dst.Info, dst.Info,
src.Storage.Info,
dst.Storage.Info,
src.FirstLayer, src.FirstLayer,
dst.FirstLayer, dst.FirstLayer,
src.FirstLevel, src.FirstLevel,
@@ -310,6 +314,8 @@ namespace Ryujinx.Graphics.Vulkan
dstImage, dstImage,
src.Info, src.Info,
dst.Info, dst.Info,
src.Storage.Info,
dst.Storage.Info,
src.FirstLayer, src.FirstLayer,
dst.FirstLayer, dst.FirstLayer,
src.FirstLevel, src.FirstLevel,
@@ -385,6 +391,8 @@ namespace Ryujinx.Graphics.Vulkan
dst.GetImage().Get(cbs).Value, dst.GetImage().Get(cbs).Value,
src.Info, src.Info,
dst.Info, dst.Info,
src.Storage.Info,
dst.Storage.Info,
src.FirstLayer, src.FirstLayer,
dst.FirstLayer, dst.FirstLayer,
src.FirstLevel, src.FirstLevel,
@@ -410,6 +418,8 @@ namespace Ryujinx.Graphics.Vulkan
dst.GetImage().Get(cbs).Value, dst.GetImage().Get(cbs).Value,
src.Info, src.Info,
dst.Info, dst.Info,
src.Storage.Info,
dst.Storage.Info,
srcRegion, srcRegion,
dstRegion, dstRegion,
src.FirstLayer, src.FirstLayer,
@@ -463,6 +473,8 @@ namespace Ryujinx.Graphics.Vulkan
dstImage.Get(cbs).Value, dstImage.Get(cbs).Value,
src.Info, src.Info,
dst.Info, dst.Info,
src.Storage.Info,
dst.Storage.Info,
srcRegion, srcRegion,
dstRegion, dstRegion,
src.FirstLayer, src.FirstLayer,

View File

@@ -68,9 +68,7 @@ namespace Ryujinx.Graphics.Vulkan
int stride = (_stride + (alignment - 1)) & -alignment; int stride = (_stride + (alignment - 1)) & -alignment;
int newSize = (_size / _stride) * stride; int newSize = (_size / _stride) * stride;
Buffer buffer = autoBuffer.Get(cbs, 0, newSize).Value; updater.BindVertexBuffer(cbs, binding, autoBuffer, 0, newSize, (ulong)stride);
updater.BindVertexBuffer(cbs, binding, buffer, 0, (ulong)newSize, (ulong)stride);
_buffer = autoBuffer; _buffer = autoBuffer;
@@ -93,11 +91,7 @@ namespace Ryujinx.Graphics.Vulkan
if (autoBuffer != null) if (autoBuffer != null)
{ {
int offset = _offset; updater.BindVertexBuffer(cbs, binding, autoBuffer, _offset, _size, (ulong)_stride);
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);
} }
} }

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Buffers;
using VkBuffer = Silk.NET.Vulkan.Buffer; using VkBuffer = Silk.NET.Vulkan.Buffer;
namespace Ryujinx.Graphics.Vulkan namespace Ryujinx.Graphics.Vulkan
@@ -15,6 +16,10 @@ namespace Ryujinx.Graphics.Vulkan
private readonly NativeArray<ulong> _sizes; private readonly NativeArray<ulong> _sizes;
private readonly NativeArray<ulong> _strides; private readonly NativeArray<ulong> _strides;
private readonly Auto<DisposableBuffer>[] _bufferAutos;
private readonly int[] _bufferOffsetsForGet;
private readonly int[] _bufferSizesForGet;
public VertexBufferUpdater(VulkanRenderer gd) public VertexBufferUpdater(VulkanRenderer gd)
{ {
_gd = gd; _gd = gd;
@@ -23,9 +28,13 @@ namespace Ryujinx.Graphics.Vulkan
_offsets = new NativeArray<ulong>(Constants.MaxVertexBuffers); _offsets = new NativeArray<ulong>(Constants.MaxVertexBuffers);
_sizes = new NativeArray<ulong>(Constants.MaxVertexBuffers); _sizes = new NativeArray<ulong>(Constants.MaxVertexBuffers);
_strides = new NativeArray<ulong>(Constants.MaxVertexBuffers); _strides = new NativeArray<ulong>(Constants.MaxVertexBuffers);
_bufferAutos = new Auto<DisposableBuffer>[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<DisposableBuffer> autoBuffer, int offset, int size, ulong stride)
{ {
if (_count == 0) if (_count == 0)
{ {
@@ -39,9 +48,11 @@ namespace Ryujinx.Graphics.Vulkan
int index = (int)_count; int index = (int)_count;
_buffers[index] = buffer; _bufferAutos[index] = autoBuffer;
_offsets[index] = offset; _bufferOffsetsForGet[index] = offset;
_sizes[index] = size; _bufferSizesForGet[index] = size;
_offsets[index] = (ulong)offset;
_sizes[index] = (ulong)size;
_strides[index] = stride; _strides[index] = stride;
_count++; _count++;
@@ -51,12 +62,51 @@ namespace Ryujinx.Graphics.Vulkan
{ {
if (_count != 0) if (_count != 0)
{ {
int count = (int)_count;
uint baseBinding = _baseBinding;
_count = 0;
Auto<DisposableBuffer>[] autos = ArrayPool<Auto<DisposableBuffer>>.Shared.Rent(count);
Span<int> getOffsets = stackalloc int[Constants.MaxVertexBuffers];
Span<int> getSizes = stackalloc int[Constants.MaxVertexBuffers];
Span<ulong> offsets = stackalloc ulong[Constants.MaxVertexBuffers];
Span<ulong> sizes = stackalloc ulong[Constants.MaxVertexBuffers];
Span<ulong> strides = stackalloc ulong[Constants.MaxVertexBuffers];
Span<VkBuffer> buffers = stackalloc VkBuffer[Constants.MaxVertexBuffers];
for (int i = 0; i < count; i++)
{
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];
}
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) if (_gd.Capabilities.SupportsExtendedDynamicState)
{ {
_gd.ExtendedDynamicStateApi.CmdBindVertexBuffers2( _gd.ExtendedDynamicStateApi.CmdBindVertexBuffers2(
cbs.CommandBuffer, cbs.CommandBuffer,
_baseBinding, baseBinding,
_count, (uint)count,
_buffers.Pointer, _buffers.Pointer,
_offsets.Pointer, _offsets.Pointer,
_sizes.Pointer, _sizes.Pointer,
@@ -64,10 +114,13 @@ namespace Ryujinx.Graphics.Vulkan
} }
else else
{ {
_gd.Api.CmdBindVertexBuffers(cbs.CommandBuffer, _baseBinding, _count, _buffers.Pointer, _offsets.Pointer); _gd.Api.CmdBindVertexBuffers(cbs.CommandBuffer, baseBinding, (uint)count, _buffers.Pointer, _offsets.Pointer);
}
}
finally
{
ArrayPool<Auto<DisposableBuffer>>.Shared.Return(autos, clearArray: true);
} }
_count = 0;
} }
} }

View File

@@ -494,6 +494,8 @@ namespace Ryujinx.Graphics.Vulkan
UniformBufferStandardLayout = supportedPhysicalDeviceVulkan12Features.UniformBufferStandardLayout, UniformBufferStandardLayout = supportedPhysicalDeviceVulkan12Features.UniformBufferStandardLayout,
UniformAndStorageBuffer8BitAccess = supportedPhysicalDeviceVulkan12Features.UniformAndStorageBuffer8BitAccess, UniformAndStorageBuffer8BitAccess = supportedPhysicalDeviceVulkan12Features.UniformAndStorageBuffer8BitAccess,
StorageBuffer8BitAccess = supportedPhysicalDeviceVulkan12Features.StorageBuffer8BitAccess, StorageBuffer8BitAccess = supportedPhysicalDeviceVulkan12Features.StorageBuffer8BitAccess,
ShaderSampledImageArrayNonUniformIndexing = supportedPhysicalDeviceVulkan12Features.ShaderSampledImageArrayNonUniformIndexing,
ShaderStorageImageArrayNonUniformIndexing = supportedPhysicalDeviceVulkan12Features.ShaderStorageImageArrayNonUniformIndexing,
}; };
pExtendedFeatures = &featuresVk12; pExtendedFeatures = &featuresVk12;

View File

@@ -435,8 +435,8 @@ namespace Ryujinx.Graphics.Vulkan
_physicalDevice.IsDeviceExtensionPresent(ExtExtendedDynamicState.ExtensionName), _physicalDevice.IsDeviceExtensionPresent(ExtExtendedDynamicState.ExtensionName),
features2.Features.MultiViewport && !(IsMoltenVk && Vendor == Vendor.Amd), // Workaround for AMD on MoltenVK issue features2.Features.MultiViewport && !(IsMoltenVk && Vendor == Vendor.Amd), // Workaround for AMD on MoltenVK issue
featuresRobustness2.NullDescriptor || IsMoltenVk, featuresRobustness2.NullDescriptor || IsMoltenVk,
supportsPushDescriptors && !IsMoltenVk, supportsPushDescriptors,
propertiesPushDescriptor.MaxPushDescriptors, IsMoltenVk ? 16 : propertiesPushDescriptor.MaxPushDescriptors, // In case an old version of MoltenVK is used, apply a limit to prevent vertex explosions.
featuresPrimitiveTopologyListRestart.PrimitiveTopologyListRestart, featuresPrimitiveTopologyListRestart.PrimitiveTopologyListRestart,
featuresPrimitiveTopologyListRestart.PrimitiveTopologyPatchListRestart, featuresPrimitiveTopologyListRestart.PrimitiveTopologyPatchListRestart,
supportsTransformFeedback, supportsTransformFeedback,
@@ -775,7 +775,11 @@ namespace Ryujinx.Graphics.Vulkan
supportsShaderBallot: false, supportsShaderBallot: false,
supportsShaderBarrierDivergence: Vendor != Vendor.Intel, supportsShaderBarrierDivergence: Vendor != Vendor.Intel,
supportsShaderFloat64: Capabilities.SupportsShaderFloat64, supportsShaderFloat64: Capabilities.SupportsShaderFloat64,
supportsTextureGatherOffsets: features2.Features.ShaderImageGatherExtended && !IsMoltenVk,
supportsShaderNonUniformIndexing:
featuresVk12.ShaderSampledImageArrayNonUniformIndexing &&
featuresVk12.ShaderStorageImageArrayNonUniformIndexing,
supportsTextureGatherOffsets: features2.Features.ShaderImageGatherExtended,
supportsTextureShadowLod: false, supportsTextureShadowLod: false,
supportsVertexStoreAndAtomics: features2.Features.VertexPipelineStoresAndAtomics, supportsVertexStoreAndAtomics: features2.Features.VertexPipelineStoresAndAtomics,
supportsViewportIndexVertexTessellation: featuresVk12.ShaderOutputViewportIndex, supportsViewportIndexVertexTessellation: featuresVk12.ShaderOutputViewportIndex,

View File

@@ -391,12 +391,12 @@ namespace Ryujinx.Graphics.Vulkan
{ {
if (_effect != null) if (_effect != null)
{ {
_gd.FlushAllCommands();
_gd.CommandBufferPool.Return( _gd.CommandBufferPool.Return(
cbs, cbs,
null, null,
[PipelineStageFlags.ColorAttachmentOutputBit], [PipelineStageFlags.ColorAttachmentOutputBit],
null); null);
_gd.FlushAllCommands();
cbs.GetFence().Wait(); cbs.GetFence().Wait();
cbs = _gd.CommandBufferPool.Rent(); cbs = _gd.CommandBufferPool.Rent();
} }
@@ -455,6 +455,8 @@ namespace Ryujinx.Graphics.Vulkan
ImageLayout.General, ImageLayout.General,
ImageLayout.PresentSrcKhr); ImageLayout.PresentSrcKhr);
_gd.FlushAllCommands();
_gd.CommandBufferPool.Return( _gd.CommandBufferPool.Return(
cbs, cbs,
[_imageAvailableSemaphores[semaphoreIndex]], [_imageAvailableSemaphores[semaphoreIndex]],

View File

@@ -14,7 +14,10 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" >
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,4 +1,5 @@
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Logging;
using System; using System;
namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.LibraryAppletProxy namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.LibraryAppletProxy
@@ -61,6 +62,19 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib
return ResultCode.Success; 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)] [CommandCmif(11)]
// GetLibraryAppletInfo() -> nn::am::service::LibraryAppletInfo // GetLibraryAppletInfo() -> nn::am::service::LibraryAppletInfo
public ResultCode GetLibraryAppletInfo(ServiceCtx context) public ResultCode GetLibraryAppletInfo(ServiceCtx context)
@@ -83,7 +97,8 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib
AppletIdentifyInfo appletIdentifyInfo = new() AppletIdentifyInfo appletIdentifyInfo = new()
{ {
AppletId = AppletId.QLaunch, AppletId = AppletId.QLaunch,
TitleId = 0x0100000000001000, // 0x4 padding
TitleId = 0x0100000000001000, // qlaunch systemAppletMenu title ID
}; };
context.ResponseData.WriteStruct(appletIdentifyInfo); context.ResponseData.WriteStruct(appletIdentifyInfo);

View File

@@ -28,21 +28,61 @@ namespace Ryujinx.HLE.Loaders.Processes
private ulong _latestPid; private ulong _latestPid;
private readonly object _pidLock = new();
#nullable enable #nullable enable
public ProcessResult? ActiveApplication public ProcessResult? ActiveApplication
{ {
get get
{ {
return _processesByPid.GetValueOrDefault(_latestPid); lock (_pidLock)
{
// Check if _latestPid is still valid
if (_latestPid == 0)
{
return null;
}
// Using this if statement locks up the UI and prevents a new game from loading. // Verify process still exists in kernel (authoritative source)
// Haven't quite deduced why yet. 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");
if (!_processesByPid.TryGetValue(_latestPid, out ProcessResult value)) _processesByPid.TryRemove(_latestPid, out _);
throw new RyujinxException( _latestPid = 0;
$"The HLE Process map did not have a process with ID {_latestPid}. Are you missing firmware?"); TitleIDs.CurrentApplication.Value = null;
return value; 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 #nullable disable
@@ -285,5 +325,39 @@ namespace Ryujinx.HLE.Loaders.Processes
return false; return false;
} }
/// <summary>
/// Clears a specific process from the ProcessLoader's tracking.
/// This should be called when a process exits or is terminated.
/// </summary>
/// <param name="pid">The process ID to clear</param>
public void ClearProcess(ulong pid)
{
lock (_pidLock)
{
if (_processesByPid.TryRemove(pid, out _))
{
if (_latestPid == pid)
{
_latestPid = 0;
TitleIDs.CurrentApplication.Value = null;
}
}
}
}
/// <summary>
/// Clears all processes from the ProcessLoader's tracking.
/// This should be called during system shutdown.
/// </summary>
public void ClearAllProcesses()
{
lock (_pidLock)
{
_processesByPid.Clear();
_latestPid = 0;
TitleIDs.CurrentApplication.Value = null;
}
}
} }
} }

View File

@@ -183,6 +183,7 @@ namespace Ryujinx.HLE
{ {
if (disposing) if (disposing)
{ {
Processes.ClearAllProcesses();
System.Dispose(); System.Dispose();
AudioDeviceDriver.Dispose(); AudioDeviceDriver.Dispose();
FileSystem.Dispose(); FileSystem.Dispose();

View File

@@ -1,3 +1,4 @@
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Hid; using Ryujinx.HLE.HOS.Services.Hid;
using SDL; using SDL;
using static SDL.SDL3; using static SDL.SDL3;
@@ -13,6 +14,7 @@ namespace Ryujinx.Input.SDL3
private readonly SDL_hid_device* _hidHandle; private readonly SDL_hid_device* _hidHandle;
private int _globalCount; private int _globalCount;
private ulong _lastWriteTicks;
private NpadHdRumble(SDL_hid_device* hidHandle) private NpadHdRumble(SDL_hid_device* hidHandle)
{ {
@@ -28,7 +30,7 @@ namespace Ryujinx.Input.SDL3
} }
ushort product = SDL_GetGamepadProduct(gamepadHandle); ushort product = SDL_GetGamepadProduct(gamepadHandle);
if (product != 0x2006 && product != 0x2007 && product != 0x2009 && product != 0x200e) if (!Enum.IsDefined(typeof(HDRumbleSupported), product))
{ {
return null; return null;
} }
@@ -37,7 +39,7 @@ namespace Ryujinx.Input.SDL3
} }
// Some of the code was translated from https://github.com/MIZUSHIKI/JoyShockLibrary-plus-HDRumble // Some of the code was translated from https://github.com/MIZUSHIKI/JoyShockLibrary-plus-HDRumble
private void WriteHdRumble( private bool WriteHdRumble(
int encLeftLowFreq, int encLeftLowAmp, int encLeftLowFreq, int encLeftLowAmp,
int encLeftHighFreq, int encLeftHighAmp, int encLeftHighFreq, int encLeftHighAmp,
int encRightLowFreq, int encRightLowAmp, int encRightLowFreq, int encRightLowAmp,
@@ -65,26 +67,35 @@ namespace Ryujinx.Input.SDL3
fixed (byte* ptr = buf) fixed (byte* ptr = buf)
{ {
SDL_hid_write(_hidHandle, ptr, (nuint)buf.Length); 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) private static int EncodeLowFreq(float lowFreq)
{ {
float lf = Math.Clamp(lowFreq, 40.875885f, 626.286133f); float lf = Math.Clamp(lowFreq, 40.875885f, 626.286133f);
return (int)Math.Round(32 * Math.Log2(lf * 0.1f)) - 0x40; return (int) Math.Round(32 * Math.Log2(lf * 0.1f) - 0x40);
} }
private static int EncodeHighFreq(float highFreq) private static int EncodeHighFreq(float highFreq)
{ {
float hf = Math.Clamp(highFreq, 81.75177f, 1252.572266f); float hf = Math.Clamp(highFreq, 81.75177f, 1252.572266f);
return ((int)Math.Round(32 * Math.Log2(hf * 0.1f)) - 0x60) * 4; return (int) Math.Round((32 * Math.Log2(hf * 0.1f) - 0x60) * 4);
} }
private static int EncodeLowAmp(float rawAmp) private static int EncodeLowAmp(float rawAmp)
{ {
int encodedAmp = 0; double encodedAmp = 0;
if (rawAmp is > 0 and < 0.012f) if (rawAmp is > 0 and < 0.012f)
{ {
@@ -92,15 +103,15 @@ namespace Ryujinx.Input.SDL3
} }
else if (rawAmp is >= 0.012f and < 0.112f) else if (rawAmp is >= 0.012f and < 0.112f)
{ {
encodedAmp = (int)Math.Round(4 * Math.Log2(rawAmp * 110f)); encodedAmp = 4 * Math.Log2(rawAmp * 110f);
} }
else if (rawAmp is >= 0.112f and < 0.225f) else if (rawAmp is >= 0.112f and < 0.225f)
{ {
encodedAmp = (int)Math.Round(16 * Math.Log2(rawAmp * 17f)); encodedAmp = 16 * Math.Log2(rawAmp * 17f);
} }
else if (rawAmp is >= 0.225f and <= 1f) else if (rawAmp is >= 0.225f and <= 1f)
{ {
encodedAmp = (int)Math.Round(32 * Math.Log2(rawAmp * 8.7f)); encodedAmp = 32 * Math.Log2(rawAmp * 8.7f);
} }
return (int)Math.Floor(encodedAmp / 2.0) + 64; return (int)Math.Floor(encodedAmp / 2.0) + 64;
@@ -108,7 +119,7 @@ namespace Ryujinx.Input.SDL3
private static int EncodeHighAmp(float rawAmp) private static int EncodeHighAmp(float rawAmp)
{ {
int encodedAmp = 0; double encodedAmp = 0;
if (rawAmp is > 0 and < 0.012f) if (rawAmp is > 0 and < 0.012f)
{ {
@@ -116,23 +127,23 @@ namespace Ryujinx.Input.SDL3
} }
else if (rawAmp is >= 0.012f and < 0.112f) else if (rawAmp is >= 0.012f and < 0.112f)
{ {
encodedAmp = (int)Math.Round(4 * Math.Log2(rawAmp * 110f)); encodedAmp = 4 * Math.Log2(rawAmp * 110f);
} }
else if (rawAmp is >= 0.112f and < 0.225f) else if (rawAmp is >= 0.112f and < 0.225f)
{ {
encodedAmp = (int)Math.Round(16 * Math.Log2(rawAmp * 17f)); encodedAmp = 16 * Math.Log2(rawAmp * 17f);
} }
else if (rawAmp is >= 0.225f and <= 1f) else if (rawAmp is >= 0.225f and <= 1f)
{ {
encodedAmp = (int)Math.Round(32 * Math.Log2(rawAmp * 8.7f)); encodedAmp = 32 * Math.Log2(rawAmp * 8.7f);
} }
return encodedAmp * 2; return (int) Math.Round(encodedAmp * 2);
} }
public bool HdRumble(VibrationValue left, VibrationValue right) public bool HdRumble(VibrationValue left, VibrationValue right)
{ {
WriteHdRumble(EncodeLowFreq(left.FrequencyLow), return WriteHdRumble(EncodeLowFreq(left.FrequencyLow),
EncodeLowAmp(left.AmplitudeLow), EncodeLowAmp(left.AmplitudeLow),
EncodeHighFreq(left.FrequencyHigh), EncodeHighFreq(left.FrequencyHigh),
EncodeHighAmp(left.AmplitudeHigh), EncodeHighAmp(left.AmplitudeHigh),
@@ -140,7 +151,34 @@ namespace Ryujinx.Input.SDL3
EncodeLowAmp(right.AmplitudeLow), EncodeLowAmp(right.AmplitudeLow),
EncodeHighFreq(right.FrequencyHigh), EncodeHighFreq(right.FrequencyHigh),
EncodeHighAmp(right.AmplitudeHigh)); EncodeHighAmp(right.AmplitudeHigh));
return true; }
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() public void Dispose()
@@ -148,4 +186,18 @@ namespace Ryujinx.Input.SDL3
SDL_hid_close(_hidHandle); 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
}
} }

View File

@@ -197,10 +197,12 @@ namespace Ryujinx.Input.SDL3
return _hdRumble?.HdRumble(left, right) ?? false; 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) if ((Features & GamepadFeaturesFlag.Rumble) == 0)
return; {
return false;
}
ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue); ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue);
ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue); ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue);
@@ -219,6 +221,15 @@ namespace Ryujinx.Input.SDL3
if (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs)) if (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs))
Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller."); 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) public Vector3 GetMotionData(MotionInputId inputId)

View File

@@ -162,7 +162,7 @@ namespace Ryujinx.Input.SDL3
public void SetTriggerThreshold(float triggerThreshold) public void SetTriggerThreshold(float triggerThreshold)
{ {
// No operations
} }
public bool HDRumble(VibrationValue left, VibrationValue right) public bool HDRumble(VibrationValue left, VibrationValue right)
@@ -170,10 +170,12 @@ namespace Ryujinx.Input.SDL3
return _hdRumble?.HdRumble(left, right) ?? false; 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) if ((Features & GamepadFeaturesFlag.Rumble) == 0)
return; {
return false;
}
ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue); ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue);
ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue); ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue);
@@ -192,6 +194,15 @@ namespace Ryujinx.Input.SDL3
if (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs)) if (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs))
Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller."); 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) public Vector3 GetMotionData(MotionInputId inputId)

View File

@@ -1,4 +1,7 @@
using Gommon;
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Hid;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
@@ -61,7 +64,14 @@ namespace Ryujinx.Input.SDL3
return left.IsPressed(inputId) || right.IsPressed(inputId); 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) if (lowFrequency != 0)
{ {
@@ -78,6 +88,15 @@ namespace Ryujinx.Input.SDL3
left.Rumble(0, 0, durationMs); left.Rumble(0, 0, durationMs);
right.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) public void SetConfiguration(InputConfig configuration)

View File

@@ -1,6 +1,7 @@
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Keyboard; using Ryujinx.Common.Configuration.Hid.Keyboard;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Hid;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Numerics;
@@ -396,9 +397,14 @@ namespace Ryujinx.Input.SDL3
// No operations // 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) public Vector3 GetMotionData(MotionInputId inputId)

View File

@@ -1,5 +1,6 @@
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Hid;
using System; using System;
using System.Drawing; using System.Drawing;
using System.Numerics; using System.Numerics;
@@ -67,7 +68,12 @@ namespace Ryujinx.Input.SDL3
throw new NotImplementedException(); 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(); throw new NotImplementedException();
} }

View File

@@ -5,7 +5,6 @@ using Ryujinx.Common.Configuration.Hid.Controller.Motion;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Hid; using Ryujinx.HLE.HOS.Services.Hid;
using System; using System;
using System.Buffers;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@@ -555,34 +554,37 @@ namespace Ryujinx.Input.HLE
{ {
if (queue.TryDequeue(out (VibrationValue, VibrationValue) dualVibrationValue)) if (queue.TryDequeue(out (VibrationValue, VibrationValue) dualVibrationValue))
{ {
if (_config is StandardControllerInputConfig controllerConfig && controllerConfig.Rumble.EnableRumble) if (_config is not StandardControllerInputConfig controllerConfig ||
!controllerConfig.Rumble.EnableRumble)
{ {
return;
}
VibrationValue leftVibrationValue = dualVibrationValue.Item1; VibrationValue leftVibrationValue = dualVibrationValue.Item1;
VibrationValue rightVibrationValue = dualVibrationValue.Item2; 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));
leftVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble; leftVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble;
leftVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble; leftVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble;
rightVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble; rightVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble;
rightVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble; rightVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble;
if (_gamepad?.HDRumble(leftVibrationValue, rightVibrationValue) == false) if (!controllerConfig.Rumble.UseHDRumble || _gamepad?.HDRumble(leftVibrationValue, rightVibrationValue) == false)
{ {
_gamepad?.Rumble(low, high, uint.MaxValue); 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} " + Logger.Debug?.Print(LogClass.Hid, $"Effect for {controllerConfig.PlayerIndex} " +
$"L.low.amp={leftVibrationValue.AmplitudeLow}, " + // Value=value/multiplier * multiplier (result)
$"L.high.amp={leftVibrationValue.AmplitudeHigh}, " + $"L.low.amp={leftVibrationValue.AmplitudeLow / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.AmplitudeLow}), " +
$"L.low.freq={leftVibrationValue.FrequencyLow}, " + $"L.high.amp={leftVibrationValue.AmplitudeHigh / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.AmplitudeHigh}), " +
$"L.high.freq={leftVibrationValue.FrequencyHigh}, " + $"L.low.freq={leftVibrationValue.FrequencyLow / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.FrequencyLow}), " +
$"R.low.amp={rightVibrationValue.AmplitudeLow}, " + $"L.high.freq={leftVibrationValue.FrequencyHigh / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.FrequencyHigh}), " +
$"R.high.amp={rightVibrationValue.AmplitudeHigh} " + $"R.low.amp={rightVibrationValue.AmplitudeLow / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.AmplitudeLow}), " +
$"R.low.freq={rightVibrationValue.FrequencyLow}, " + $"R.high.amp={rightVibrationValue.AmplitudeHigh / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.AmplitudeHigh}), " +
$"R.high.freq={rightVibrationValue.FrequencyHigh}"); $"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})");
} }
} }
} }

View File

@@ -80,10 +80,7 @@ namespace Ryujinx.Input
/// </summary> /// </summary>
/// <param name="left">The vibration data for the left side</param> /// <param name="left">The vibration data for the left side</param>
/// <param name="right">The vibration data for the right side</param> /// <param name="right">The vibration data for the right side</param>
bool HDRumble(VibrationValue left, VibrationValue right) bool HDRumble(VibrationValue left, VibrationValue right);
{
return false;
}
/// <summary> /// <summary>
/// Starts a rumble effect on the gamepad. /// Starts a rumble effect on the gamepad.
@@ -91,10 +88,10 @@ namespace Ryujinx.Input
/// <param name="lowFrequency">The intensity of the low frequency from 0.0f to 1.0f</param> /// <param name="lowFrequency">The intensity of the low frequency from 0.0f to 1.0f</param>
/// <param name="highFrequency">The intensity of the high frequency from 0.0f to 1.0f</param> /// <param name="highFrequency">The intensity of the high frequency from 0.0f to 1.0f</param>
/// <param name="durationMs">The duration of the rumble effect in milliseconds.</param> /// <param name="durationMs">The duration of the rumble effect in milliseconds.</param>
void Rumble(float lowFrequency, float highFrequency, uint durationMs); bool Rumble(float lowFrequency, float highFrequency, uint durationMs);
/// <summary> /// <summary>
/// Get a snaphost of the state of the gamepad that is remapped with the informations from the <see cref="InputConfig"/> set via <see cref="SetConfiguration(InputConfig)"/>. /// Get a snaphost of the state of the gamepad that is remapped with the information from the <see cref="InputConfig"/> set via <see cref="SetConfiguration(InputConfig)"/>.
/// </summary> /// </summary>
/// <returns>A remapped snaphost of the state of the gamepad.</returns> /// <returns>A remapped snaphost of the state of the gamepad.</returns>
GamepadStateSnapshot GetMappedStateSnapshot(); GamepadStateSnapshot GetMappedStateSnapshot();

View File

@@ -221,6 +221,7 @@ namespace Ryujinx.Headless
StrongRumble = 1f, StrongRumble = 1f,
WeakRumble = 1f, WeakRumble = 1f,
EnableRumble = false, EnableRumble = false,
UseHDRumble = true
}, },
}; };
} }

View File

@@ -1,6 +1,7 @@
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Keyboard; using Ryujinx.Common.Configuration.Hid.Keyboard;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Hid;
using Ryujinx.Input; using Ryujinx.Input;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -149,9 +150,20 @@ namespace Ryujinx.Ava.Input
Logger.Info?.Print(LogClass.UI, "SetLed called on an AvaloniaKeyboard"); 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; public Vector3 GetMotionData(MotionInputId inputId) => Vector3.Zero;

View File

@@ -1,5 +1,6 @@
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Hid;
using Ryujinx.Input; using Ryujinx.Input;
using System; using System;
using System.Drawing; using System.Drawing;
@@ -65,7 +66,12 @@ namespace Ryujinx.Ava.Input
throw new NotImplementedException(); 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(); throw new NotImplementedException();
} }

View File

@@ -46,6 +46,7 @@ namespace Ryujinx.Ava
private const uint MbIconwarning = 0x30; private const uint MbIconwarning = 0x30;
[STAThread]
public static int Main(string[] args) public static int Main(string[] args)
{ {
Version = ReleaseInformation.Version; Version = ReleaseInformation.Version;
@@ -54,17 +55,39 @@ namespace Ryujinx.Ava
{ {
if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041)) if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041))
{ {
_ = Win32NativeInterop.MessageBoxA(nint.Zero, "You are running an outdated version of Windows.\n\nRyujinx supports Windows 10 version 20H1 and newer.\n", $"Ryujinx {Version}", MbIconwarning); Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run on an outdated version of Windows. Exiting...");
_ = Win32NativeInterop.MessageBoxA(nint.Zero,
"You are running an outdated version of Windows.\n\nRyujinx supports Windows 10 version 20H1 and newer.\n",
$"Ryujinx {Version}", MbIconwarning);
return 0; return 0;
} }
var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); string onedriveFiles = Environment.GetEnvironmentVariable("Onedrive");
var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); string onedriveConsumerFiles = Environment.GetEnvironmentVariable("OnedriveConsumer");
string onedriveCommercialFiles = Environment.GetEnvironmentVariable("OnedriveCommercial");
// Apparently not everyone has OneDrive shoved onto their system.
if ((onedriveFiles is not null && Environment.CurrentDirectory.StartsWithIgnoreCase(onedriveFiles))
|| (onedriveConsumerFiles is not null && Environment.CurrentDirectory.StartsWithIgnoreCase(onedriveConsumerFiles))
|| (onedriveCommercialFiles is not null && Environment.CurrentDirectory.StartsWithIgnoreCase(onedriveCommercialFiles)))
{
Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run from a OneDrive folder. Exiting...");
_ = Win32NativeInterop.MessageBoxA(nint.Zero,
"Ryujinx is not intended to be run from a OneDrive folder. Please move it out and relaunch.",
$"Ryujinx {Version}", MbIconwarning);
return 0;
}
string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
string programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
if (Environment.CurrentDirectory.StartsWithIgnoreCase(programFiles) || if (Environment.CurrentDirectory.StartsWithIgnoreCase(programFiles) ||
Environment.CurrentDirectory.StartsWithIgnoreCase(programFilesX86)) Environment.CurrentDirectory.StartsWithIgnoreCase(programFilesX86))
{ {
_ = Win32NativeInterop.MessageBoxA(nint.Zero, "Ryujinx is not intended to be run from the Program Files folder. Please move it out and relaunch.", $"Ryujinx {Version}", MbIconwarning); Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run from the Program Files folder. Exiting...");
_ = Win32NativeInterop.MessageBoxA(nint.Zero,
"Ryujinx is not intended to be run from the Program Files folder. Please move it out and relaunch.",
$"Ryujinx {Version}", MbIconwarning);
return 0; return 0;
} }
@@ -74,10 +97,70 @@ namespace Ryujinx.Ava
// ...but this reads like it checks if the current is in/has the Windows admin role? lol // ...but this reads like it checks if the current is in/has the Windows admin role? lol
if (new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator)) if (new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator))
{ {
_ = Win32NativeInterop.MessageBoxA(nint.Zero, "Ryujinx is not intended to be run as administrator.", $"Ryujinx {Version}", MbIconwarning); Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run as administrator. Exiting...");
_ = Win32NativeInterop.MessageBoxA(nint.Zero, "Ryujinx is not intended to be run as administrator.",
$"Ryujinx {Version}", MbIconwarning);
return 0; return 0;
} }
} }
else // Unix
{
// sudo check
[DllImport("libc")]
static extern uint geteuid();
bool root = geteuid().Equals(0);
if (OperatingSystem.IsMacOS())
{
if (root)
{
Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run as administrator. Exiting...");
macOSNativeInterop.SimpleMessageBox($"Ryujinx {Version}",
"Ryujinx is not intended to be run as administrator.", "Ok");
return 0;
}
string downloadFiles = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads");
if (Environment.CurrentDirectory.StartsWithIgnoreCase(downloadFiles))
{
Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run from the Downloads folder. Exiting...");
macOSNativeInterop.SimpleMessageBox($"Ryujinx {Version}",
"Ryujinx is not intended to be run from the Downloads folder.", "Ok");
return 0;
}
string icloudFiles = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library/Mobile Documents/com~apple~CloudDocs");
if (Environment.CurrentDirectory.StartsWithIgnoreCase(icloudFiles))
{
Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run from the iCloud folder. Exiting...");
macOSNativeInterop.SimpleMessageBox($"Ryujinx {Version}",
"Ryujinx is not intended to be run from the iCloud folder.", "Ok");
return 0;
}
}
if (OperatingSystem.IsLinux())
{
if (root)
{
Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run as administrator. Exiting...");
LinuxSDLInterop.SimpleMessageBox($"Ryujinx {Version}", "Ryujinx is not intended to be run as administrator.");
return 0;
}
string container = Environment.GetEnvironmentVariable("container");
if (container is not null && container.EqualsIgnoreCase("flatpak"))
{
Logger.Info?.PrintMsg(LogClass.Application, "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=");
Logger.Warning?.PrintMsg(LogClass.Application, "This is very likely an unofficial build, Ryujinx does NOT have a flatpak!");
Logger.Info?.PrintMsg(LogClass.Application, "Please visit https://ryujinx.app/ for our official builds.");
Logger.Info?.PrintMsg(LogClass.Application, "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=");
}
}
}
bool noGuiArg = ConsumeCommandLineArgument(ref args, "--no-gui") || ConsumeCommandLineArgument(ref args, "nogui"); bool noGuiArg = ConsumeCommandLineArgument(ref args, "--no-gui") || ConsumeCommandLineArgument(ref args, "nogui");
bool coreDumpArg = ConsumeCommandLineArgument(ref args, "--core-dumps"); bool coreDumpArg = ConsumeCommandLineArgument(ref args, "--core-dumps");
@@ -310,7 +393,7 @@ namespace Ryujinx.Ava
"never" => HideCursorMode.Never, "never" => HideCursorMode.Never,
"onidle" => HideCursorMode.OnIdle, "onidle" => HideCursorMode.OnIdle,
"always" => HideCursorMode.Always, "always" => HideCursorMode.Always,
_ => ConfigurationState.Instance.HideCursor, _ => ConfigurationState.Instance.HideCursor
}; };
// Check if memoryManagerMode was overridden. // Check if memoryManagerMode was overridden.

View File

@@ -63,7 +63,7 @@
<PackageReference Include="OpenTK.Core" /> <PackageReference Include="OpenTK.Core" />
<PackageReference Include="Ryujinx.Audio.OpenAL" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'osx-x64' AND '$(RuntimeIdentifier)' != 'osx-arm64'" /> <PackageReference Include="Ryujinx.Audio.OpenAL" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'osx-x64' AND '$(RuntimeIdentifier)' != 'osx-arm64'" />
<PackageReference Include="Ryujinx.Graphics.Nvdec.Dependencies.AllArch" /> <PackageReference Include="Ryujinx.Graphics.Nvdec.Dependencies.AllArch" />
<PackageReference Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'win-x64' AND '$(RuntimeIdentifier)' != 'win-arm64'" /> <PackageReference Include="Ryujinx.Graphics.Vulkan.MoltenVK" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'win-x64' AND '$(RuntimeIdentifier)' != 'win-arm64'" />
<PackageReference Include="Ryujinx.UpdateClient" /> <PackageReference Include="Ryujinx.UpdateClient" />
<PackageReference Include="Ryujinx.Systems.Update.Common" /> <PackageReference Include="Ryujinx.Systems.Update.Common" />
<PackageReference Include="securifybv.ShellLink" /> <PackageReference Include="securifybv.ShellLink" />

View File

@@ -17,7 +17,7 @@ namespace Ryujinx.Ava.Systems.Configuration
/// <summary> /// <summary>
/// The current version of the file format /// The current version of the file format
/// </summary> /// </summary>
public const int CurrentVersion = 72; public const int CurrentVersion = 73;
/// <summary> /// <summary>
/// Version of the configuration file format /// Version of the configuration file format

View File

@@ -333,6 +333,7 @@ namespace Ryujinx.Ava.Systems.Configuration
EnableRumble = false, EnableRumble = false,
StrongRumble = 1f, StrongRumble = 1f,
WeakRumble = 1f, WeakRumble = 1f,
UseHDRumble = true
}; };
} }
} }

View File

@@ -1070,6 +1070,23 @@ namespace Ryujinx.Ava.Systems.PlayReport
_ => FormattedValue.ForceReset _ => FormattedValue.ForceReset
}; };
private static FormattedValue TomodachiLifeLTD_Status(SingleValue value)
{
MessagePackObject messagePackObject = value.Matched.PackedValue;
MessagePackObjectDictionary messagePackObjectDictionary = messagePackObject.AsDictionary();
int miiCount = messagePackObjectDictionary["MiiNum"].AsInt32();
int fountainLevel = messagePackObjectDictionary["FountainLevel"].AsInt32();
return $"Looking after {"Mii".ToQuantity(miiCount)}, with an island level of {fountainLevel}";
}
private static FormattedValue AnimalCrossingNewHorizons_AppCommon(SingleValue value)
{
MessagePackObject messagePackObject = value.Matched.PackedValue;
MessagePackObjectDictionary messagePackObjectDictionary = messagePackObject.AsDictionary();
return $"Living on {messagePackObjectDictionary["LandName"].AsString()} Island";
}
} }
} }

View File

@@ -119,6 +119,19 @@ namespace Ryujinx.Ava.Systems.PlayReport
"based on what game you first launch.\n\nNSO emulators do not print any Play Report information past the first game launch so it's all we got.") "based on what game you first launch.\n\nNSO emulators do not print any Play Report information past the first game launch so it's all we got.")
.AddValueFormatter("launch_title_id", NsoEmulator_LaunchedGame) .AddValueFormatter("launch_title_id", NsoEmulator_LaunchedGame)
) )
.AddSpec(
[ "010051f0207b2000", "0100ca502552a000" ], // Tomodachi Life: Living the Dream + Demo
spec => spec
.WithDescription(
"based on your total Mii count and island level.")
.AddValueFormatter("Common", TomodachiLifeLTD_Status)
)
.AddSpec(
"01006f8002326000", // Animal Crossing New Horizons
spec => spec
.WithDescription("based on your island name.")
.AddValueFormatter("AppCmn", AnimalCrossingNewHorizons_AppCommon)
)
); );
private static string Playing(string game) => $"Playing {game}"; private static string Playing(string game) => $"Playing {game}";

View File

@@ -0,0 +1,30 @@
using System;
using System.Runtime.InteropServices;
namespace Ryujinx.Ava.UI.Helpers
{
public class LinuxSDLInterop
{
// TODO: add a parameter for prompt style
// TODO: look into adding text for the button
// TODO: check success of prompt box
public static int SimpleMessageBox(string caption, string text)
{
const string sdl = "SDL2";
[DllImport(sdl)]
static extern int SDL_Init(uint flags);
[DllImport(sdl, CallingConvention = CallingConvention.Cdecl)]
static extern int SDL_ShowSimpleMessageBox(uint flags, string title, string message, IntPtr window);
[DllImport(sdl)]
static extern void SDL_Quit();
SDL_Init(0);
SDL_ShowSimpleMessageBox(32 /* 32 = warning style */, caption, text, IntPtr.Zero);
SDL_Quit();
return 0;
}
}
}

View File

@@ -0,0 +1,62 @@
using System;
using System.Runtime.InteropServices;
namespace Ryujinx.Ava.UI.Helpers
{
public class macOSNativeInterop
{
// TODO: add a parameter for prompt style
// TODO: check success of prompt box
public static int SimpleMessageBox(string caption, string text, string button)
{
// Grab what we need to make the message box.
const string ObjCRuntime = "/usr/lib/libobjc.A.dylib";
const string FoundationFramework = "/System/Library/Frameworks/Foundation.framework/Foundation";
const string AppKitFramework = "/System/Library/Frameworks/AppKit.framework/AppKit";
[DllImport(ObjCRuntime, EntryPoint = "sel_registerName")]
static extern IntPtr GetSelector(string name);
[DllImport(ObjCRuntime, EntryPoint = "objc_getClass")]
static extern IntPtr GetClass(string name);
[DllImport(FoundationFramework, EntryPoint = "objc_msgSend")]
static extern IntPtr SendMessage(IntPtr target, IntPtr selector);
[DllImport(FoundationFramework, EntryPoint = "objc_msgSend")]
static extern IntPtr SendMessageWithParameter(IntPtr target, IntPtr selector, IntPtr param);
[DllImport(ObjCRuntime)]
static extern IntPtr dlopen(string path, int mode);
dlopen(AppKitFramework, 0x1); // have to invoke AppKit so that NSAlert doesn't return a null pointer
IntPtr NSStringClass = GetClass("NSString");
IntPtr Selector = GetSelector("stringWithUTF8String:");
IntPtr SharedApp = SendMessage(GetClass("NSApplication"), GetSelector("sharedApplication"));
IntPtr NSAlert = SendMessage(GetClass("NSAlert"), GetSelector("alloc"));
IntPtr AlertInstance = SendMessage(NSAlert, GetSelector("init"));
// Create caption, text, and button text.
IntPtr boxCaption = SendMessageWithParameter(NSStringClass, Selector, Marshal.StringToHGlobalAnsi(caption));
IntPtr boxText = SendMessageWithParameter(NSStringClass, Selector, Marshal.StringToHGlobalAnsi(text));
IntPtr boxButton = SendMessageWithParameter(NSStringClass, Selector, Marshal.StringToHGlobalAnsi(button));
// Set up the window.
SendMessageWithParameter(SharedApp, GetSelector("setActivationPolicy:"), IntPtr.Zero); // Give it a window.
SendMessageWithParameter(SharedApp, GetSelector("activateIgnoringOtherApps:"), (IntPtr) 1); // Force it to the front.
// Set up the message box.
SendMessageWithParameter(AlertInstance, GetSelector("setAlertStyle:"), IntPtr.Zero); // Set style to warning.
SendMessageWithParameter(AlertInstance, GetSelector("setMessageText:"), boxCaption);
SendMessageWithParameter(AlertInstance, GetSelector("setInformativeText:"), boxText);
SendMessageWithParameter(AlertInstance, GetSelector("addButtonWithTitle:"), boxButton);
// Send prompt to user, then clean up.
SendMessage(AlertInstance, GetSelector("runModal"));
SendMessage(AlertInstance, GetSelector("release"));
return 0;
}
}
}

View File

@@ -20,6 +20,7 @@ namespace Ryujinx.Ava.UI.Models.Input
public float WeakRumble { get; set; } public float WeakRumble { get; set; }
public float StrongRumble { get; set; } public float StrongRumble { get; set; }
public bool UseHDRumble { get; set; }
public string Id { get; set; } public string Id { get; set; }
@@ -236,6 +237,7 @@ namespace Ryujinx.Ava.UI.Models.Input
EnableRumble = controllerInput.Rumble.EnableRumble; EnableRumble = controllerInput.Rumble.EnableRumble;
WeakRumble = controllerInput.Rumble.WeakRumble; WeakRumble = controllerInput.Rumble.WeakRumble;
StrongRumble = controllerInput.Rumble.StrongRumble; StrongRumble = controllerInput.Rumble.StrongRumble;
UseHDRumble = controllerInput.Rumble.UseHDRumble;
} }
if (controllerInput.Led != null) if (controllerInput.Led != null)
@@ -307,6 +309,7 @@ namespace Ryujinx.Ava.UI.Models.Input
EnableRumble = EnableRumble, EnableRumble = EnableRumble,
WeakRumble = WeakRumble, WeakRumble = WeakRumble,
StrongRumble = StrongRumble, StrongRumble = StrongRumble,
UseHDRumble = UseHDRumble,
}, },
Led = new LedConfigController Led = new LedConfigController
{ {

View File

@@ -789,6 +789,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
StrongRumble = 1f, StrongRumble = 1f,
WeakRumble = 1f, WeakRumble = 1f,
EnableRumble = false, EnableRumble = false,
UseHDRumble = true
}, },
}; };
} }

View File

@@ -9,5 +9,8 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
[ObservableProperty] [ObservableProperty]
public partial float WeakRumble { get; set; } public partial float WeakRumble { get; set; }
[ObservableProperty]
public partial bool EnableHDRumble { get; set; }
} }
} }

View File

@@ -53,6 +53,15 @@
Margin="5,0" Margin="5,0"
Text="{Binding WeakRumble, StringFormat=\{0:0.00\}}" /> Text="{Binding WeakRumble, StringFormat=\{0:0.00\}}" />
</StackPanel> </StackPanel>
<CheckBox
Margin="5"
IsChecked="{Binding EnableHDRumble}">
<TextBlock
Margin="0,3,0,0"
VerticalAlignment="Center"
Text="{ext:Locale ControllerSettingsRumbleUseHDRumble}"
ToolTip.Tip="{ext:Locale HDRumbleTooltip}" />
</CheckBox>
</StackPanel> </StackPanel>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@@ -22,6 +22,7 @@ namespace Ryujinx.Ava.UI.Views.Input
{ {
StrongRumble = config.StrongRumble, StrongRumble = config.StrongRumble,
WeakRumble = config.WeakRumble, WeakRumble = config.WeakRumble,
EnableHDRumble = config.UseHDRumble
}; };
InitializeComponent(); InitializeComponent();
@@ -45,6 +46,7 @@ namespace Ryujinx.Ava.UI.Views.Input
GamepadInputConfig config = viewModel.Config; GamepadInputConfig config = viewModel.Config;
config.StrongRumble = content.ViewModel.StrongRumble; config.StrongRumble = content.ViewModel.StrongRumble;
config.WeakRumble = content.ViewModel.WeakRumble; config.WeakRumble = content.ViewModel.WeakRumble;
config.UseHDRumble = content.ViewModel.EnableHDRumble;
}; };
await contentDialog.ShowAsync(); await contentDialog.ShowAsync();