Compare commits

...

97 Commits

Author SHA1 Message Date
c06f16c5e6 infra: chore: Raise minimum required Windows 10 version
Inspired by the breakages covered in #409
2025-01-23 17:39:34 -06:00
7829fd8ee7 misc: chore: OS + CPU arch helpers 2025-01-23 16:58:48 -06:00
33079422fe misc: chore: code cleanups 2025-01-23 16:47:11 -06:00
f81cb093fc misc: chore: Change references of GreemDev/Ryujinx to Ryubing/Ryujinx 2025-01-23 16:27:49 -06:00
dc0c7a2912 infra: clarify how stable builds are made now 2025-01-23 16:27:25 -06:00
1fbee5a584 infra: Metal label 2025-01-23 14:39:36 -06:00
c140e9b23c UI: Localize LED color & hide it until it's functional
Also moved IgnoreApplet to the System config section object.
2025-01-23 00:48:42 -06:00
9c8055440e HLE: TryAdd firmware NCAs 2025-01-22 23:58:11 -06:00
c03cd50fa3 UI: Add the ability to change a DualSense/DualShock 4's LED color.
Not functional yet. This is the UI & persistence side of #572.
2025-01-22 19:53:39 -06:00
069f630776 docs: compat: boots: ENDER MAGNOLIA: Bloom in the Mist 2025-01-22 18:00:14 -06:00
13d411e4de misc: chore: also ToLower the titleID for the OpenShaderDirectory button 2025-01-22 08:54:39 -06:00
9f53b07491 misc: chore: Fix shader cache & CPU cache being in different folders on non-Windows
fixes #565
2025-01-22 08:52:21 -06:00
cd8113dadf misc: chore: Collapse adding a game/autoload dir into a single reusable method. 2025-01-21 18:59:56 -06:00
9089c4ffe5 misc: chore: Multi/Single file/folder picker extensions (for convenience)
The result of these extensions is an empty Optional when the user hits Cancel on the shown file picker.
2025-01-21 18:59:19 -06:00
fe9d8d05bd UI: Fixed the Amiibo keybind only working when the UI had been updated. 2025-01-21 18:00:51 -06:00
880a8ae748 misc: chore: Remove duplicated styling blocks in MainMenuBarView in favor of a reusable Avalonia Style. 2025-01-21 17:50:55 -06:00
11531dacb6 UI: Option to automatically Hide UI when game launches (#462)
Quality of life feature
Similar in function to the "Start Games in Fullscreen" toggle
For users who want to run games in windowed/non-fullscreen mode with
menu UI hidden, this eliminates the need to always click "Hide UI"
2025-01-21 17:36:51 -06:00
3974739ed3 More descriptive classification 2025-01-21 17:35:22 -06:00
440a3447fb docs: compat: Update South Park: The Fractured but Whole: playable -> boots 2025-01-21 17:10:54 -06:00
65374ed6cb UI: [ci skip] clarify dirty hacks subtext 2025-01-21 16:57:05 -06:00
789d6ab959 misc: chore: Improve autoloading DLC/updates logging, deliberately switch to Vulkan if Metal was somehow chosen on a non-mac system, add logger lines in the updater, cleanup enabled logs printing 2025-01-21 14:59:08 -06:00
182db31343 metal: Added Persona 4 Arena Ultimax to Auto 2025-01-21 14:07:05 -06:00
ea296b134d Use specific Ubuntu version in build script 2025-01-21 14:05:49 -06:00
bf584442b2 misc: chore: remove needless call to string.Format 2025-01-21 14:05:49 -06:00
cb7c294dbf Add more games to metal game list (#558)
Link's awakening I have played through most of the game with 0 issues.

In LEGO City undercover I have played multiple missions and explored the
map, I was unable to spot any issue. Except shadows flickering
sometimes, but that seems to happen on Vulkan and the PC version as well
and is propably normal.

Bayonetta seems to work flawless so far.

In Fast RMX, some tracks have flickering issues, like the second track
of the first cup. This happens on Vulkan as well.

-

Mario Bros. Wonder has following issues:

Overall issues:
- Sometimes there is short white flickering/artifacts, but they happen
on Vulkan as well.

Metal specific issue:
- In 2 underwater levels, a specific location causes a FPS drop not
present on Vulkan. But this is very minor and on all current M chips you
get on average better FPS with Metal (On my M3, there are occasional
drops that are worse with Vulkan), reducing stutter quite noticeably,
which is why I think it should get added to auto regardless.

- Isaac mentioned there is a issue in level 2, where the flowers singing
desync somehow. However, I was after lot of testing unable to replicate
this issue at all. More testing could be useful.

Fix: Fixed 2 typos in the comments
2025-01-21 14:05:41 -06:00
eaf1e7efd2 Cleanup in TitleIDs.cs (#546)
Just a little cleanup in TitleID.cs, adding a franchise title to most
franchises + sorting in alphabetical on all games.
2025-01-21 11:20:43 -06:00
471e7ed2e4 Add TitleID sort method (#553)
Adds an additional application list sorting method for the TitleID. A
bit of a niche choice for sorting but I think the TID is a relevant
enough piece of metadata that it should be there. (And I personally
would be using it)

- Using existing TitleId constant in ApplicationSort, implying this was
meant to be in the sorting options at some point?
- Reuses the "DlcManagerTableHeadingTitleIdLabel" locale for fulfilling
the need already, might be better to make a unique one for this in the
long run but this codebase is new to me so I wanted to make the changes
as unobtrusive as possible
- Using app.Id for the comparer seems to work fine, not sure if using
something else like IdString would be better?
2025-01-21 11:06:40 -06:00
ad3e80b383 Log .NET runtime version (#552)
I was looking into a crash, and found out it was an issue that was fixed
in .NET 9.0.1. Since Ryujinx embeds the runtime into the executable, it
not obvious which runtime a build uses. This logs the .NET runtime
version immediately after the build version.
2025-01-20 19:19:19 -06:00
ed64a63094 UI: Visually merge "Actions" and "Tools" menu bar items into Actions
The contents of the menu item are dependent on whether you're in a game.
No functionality has been removed.
2025-01-20 16:56:05 -06:00
8df7ba2d56 i18n: Norwegian DLC RomFS dumping translation 2025-01-20 15:55:37 -06:00
e743d78115 Add/fix service reported info (#551)
fixes the GetConfig service call, which now returns success correctly
adds support for getting the device serial number (which is fake and
reports as "RYU00000000000")
2025-01-20 14:59:54 -06:00
04ba762710 UI: Move DLC RomFS dumping under normal RomFS dumping.
Also removed it from DLC manager.
2025-01-20 14:30:28 -06:00
f42b2ed59d misc: chore: more correct last used user checking 2025-01-20 13:33:59 -06:00
d135385cab Leftovers, oops 2025-01-20 09:32:38 -06:00
b360f4e721 UI: Dump DLC RomFS.
You can access this in the Manage DLC screen, it's the new button on each DLC line.

Closes #548
2025-01-20 09:28:58 -06:00
a1c0c70ec2 Added missing TitleIDs (#545)
Added the remaining missing TitleIDs in the compat.csv list.
Excluding Homebrew apps.
2025-01-19 21:15:46 -06:00
290ac405ac Updated Ukrainian translation by Rondo (#543)
Co-authored-by: rrondo <46533574+rrondo@users.noreply.github.com>
2025-01-19 21:00:40 -06:00
bbd64fd5f0 misc: chore: Cleanup AppletMetadata usage 2025-01-19 19:40:49 -06:00
09446fd80e [ci skip] infra: Update copyright year in shortcut plist 2025-01-19 18:57:05 -06:00
4f014a89cf docs: compat: Donkey Kong Country Returns HD 2025-01-19 18:28:43 -06:00
6482e566ab UI: Compat: Unload compatibility entries when the window closes. 2025-01-19 17:41:50 -06:00
7fcd9b792e UI: Compat: Update owned game title IDs when ApplicationLibrary app count updates 2025-01-19 17:41:31 -06:00
e676fd8b17 UI: misc: simplify Intel Mac warning logic 2025-01-19 14:42:15 -06:00
dd16e3cee1 misc: chore: very small cleanup in AvaHostUIHandler 2025-01-19 13:18:40 -06:00
31e5f74e05 UI: misc: Replace spaces in Title with newlines when using custom title bar (since the Title is in an Avalonia tooltip) 2025-01-19 13:05:20 -06:00
f2f099bddb remove Async suffixes; they're factory methods not actual async methods. 2025-01-19 12:46:32 -06:00
2616dc57fb misc: chore: RelayCommand helper 2025-01-19 12:44:07 -06:00
0cdf7cfe21 UI: Open cheat manager in catch-all try 2025-01-18 22:48:06 -06:00
2ecf999569 misc: chore: change ThemeManager ThemeChanged to a basic Action since both arguments are unused 2025-01-18 22:48:06 -06:00
b612fc5155 Updated TitleIDs (#541)
Added more games to the RPC list. Now alphabetical.
2025-01-18 22:19:28 -06:00
25eb545409 Update bug_report.yml 2025-01-18 20:55:28 -06:00
52269964b6 Add the player select applet. (#537)
This introduces the somewhat completed version of the Player Select
Applet, allowing users to select either a user or a guest from the UI.
Note: Selecting the guest more then once currently does not work.

closes https://github.com/Ryubing/Ryujinx/issues/532
2025-01-18 20:40:33 -06:00
ccdddac8fc Fix compile warnings 2025-01-18 19:34:31 -06:00
1bc30bf3ba Update locales.json (#538)
Added a missing translation line
2025-01-18 18:40:51 -06:00
4868fface8 UI: Intel Mac warning
Upon launch, shows a warning about using an Intel Mac. This will only show once every boot. You can only turn it off by getting a better system.
2025-01-18 15:33:05 -06:00
6fca4492d0 misc: chore: Remove status update event stuff in Headless 2025-01-18 15:15:08 -06:00
ade2f256e0 misc: chore: remove duplicate graphics debug levels in headless windows 2025-01-18 11:19:38 -06:00
580b150c9a Revert "infra: Conditionally compile Metal & OpenGL depending on if the target RuntimeIdentifier is mac"
This reverts commit 2f93a0f706.
2025-01-18 10:57:02 -06:00
e6bad52945 Revert "Only selectively compile Metal & fix some compilation issues"
This reverts commit beda3206e0.
2025-01-18 10:56:58 -06:00
beda3206e0 Only selectively compile Metal & fix some compilation issues 2025-01-18 10:52:32 -06:00
2f93a0f706 infra: Conditionally compile Metal & OpenGL depending on if the target RuntimeIdentifier is mac 2025-01-18 10:38:29 -06:00
80f44d9547 misc: chore: small cleanup 2025-01-18 10:33:57 -06:00
b08e5db6d8 Headless: Dispose of inputmanager in a catch-all try 2025-01-18 10:30:19 -06:00
6a291d4116 Headless: Use main UI logo for window icon instead of separate bmp 2025-01-18 10:26:12 -06:00
6fc827fe67 headless: collapse headless window definition into a "Windows" folder, change GetWindowFlags to an abstract property. 2025-01-18 10:15:24 -06:00
6cd4866d76 Updated sv_SE in locales.json (#513) 2025-01-17 18:04:18 -06:00
4d7ca5c0f0 Update Chinese translations (#375) 2025-01-17 17:35:34 -06:00
a375faecc1 UI: Fix UpdateWaitWindow.axaml windows being too big on windows (#314) 2025-01-17 14:14:19 -06:00
1728b0f20c WWE 2K18 is not playable. 2025-01-17 11:37:08 -06:00
5aa071c59b remove notice for unusual core counts (#531) 2025-01-17 05:50:42 -06:00
1018c9db8b Update Norwegian Translation (#503)
Norwegian translation updated with the Compatibility list addition
2025-01-16 10:02:33 -06:00
01ccd18726 UI: Meant to use that method in another place [ci-skip] 2025-01-16 09:52:35 -06:00
abfbc6f4bc UI: Prevent desynced RPC when toggling it off/on while in-game 2025-01-16 09:52:01 -06:00
6a4bc02d7a Update Italian translation (#489) 2025-01-16 06:38:36 -06:00
814c0526d2 Korean translations for compat list (#502) 2025-01-16 04:57:32 -06:00
a5a4ef38e6 HLE: Stub IHidServer SetGestureOutputRanges (#524)
Lets "Donkey Kong Country Returns HD" get into main gameplay.
2025-01-16 02:39:39 -06:00
c17e3bfcdf genuinely dont know how that was still there, i thought i got rid of UI.Common 2025-01-15 03:01:17 -06:00
017f46f318 HLE: misc: throw a more descriptive error when the loaded processes doesn't contain _latestPid (likely missing FW) 2025-01-15 03:01:17 -06:00
fd4d801bfd Various NuGet package updates (#203)
Updates the following packages:

**nuget: bump the avalonia group with 7 updates**

* Bump Avalonia, Avalonia.Controls.DataGrid, Avalonia.Desktop,
Avalonia.Diagnostics, and Avalonia.Markup.Xaml.Loader from 11.0.10 to
11.0.13
* Bump Avalonia.Svg and Avalonia.Svg.Skia from 11.0.0.18 to 11.0.0.19

**nuget: bump non-avalonia packages**

* Bump Concentus from 2.2.0 to 2.2.2
* Bump Microsoft.IdentityModel.JsonWebTokens from 8.1.2 to 8.3.0
* Bump Silk.NET.Vulkan group with 3 updates (2.21.0 to 2.22.0)
* Bump SkiaSharp group with 2 updates (2.88.7 to 2.88.9)
2025-01-13 11:15:05 -06:00
f1dee50275 infra: Update to LLVM 17 (#519)
This fixes macos builds not building correctly because of a missing
LLVM 14 package.
2025-01-12 14:57:57 -06:00
c2ae49eb47 Add some missing Simplified Chinese translations (#515) 2025-01-12 12:33:27 -06:00
47c71966d0 Add compat of Xenoblade 2 JP edition same as global edition (#517) 2025-01-12 12:31:58 -06:00
f9e8f4bc29 docs: compat: Ori and the Will of the Wisps is now ingame, not playable 2025-01-11 06:10:58 -06:00
7694c8c046 Do not auto release stable 2025-01-11 04:08:11 -06:00
0dd789e8a5 misc: chore: remove redundant trimming on CompatibilityEntry.GameName init 2025-01-11 01:26:34 -06:00
4e0aafd005 docs: compat: Trim redundant/duplicate information to save space 2025-01-11 01:18:10 -06:00
c5091f499e docs: compat: The House of the Dead: Remake Playable 2025-01-11 00:38:32 -06:00
41c8fd8194 misc: chore: lol this field was misspelled 2025-01-10 23:23:53 -06:00
d4a7ee25ea misc: chore: use ObservableProperty on input view models 2025-01-10 23:23:05 -06:00
3141c560fb misc: chore: remove sender parameter from LdnGameData receieved event 2025-01-10 23:15:55 -06:00
de341b285b misc: use ObservableProperty on HotkeyConfig fields 2025-01-10 23:15:37 -06:00
cc95e80ee9 misc: chore: Move converters into a directory in Helpers. Namespace unchanged 2025-01-10 20:24:53 -06:00
d75ce52bd4 UI: Show play time in one time unit, maxing out at hours. 2025-01-10 20:23:47 -06:00
4a4ea557de UI: compat: show last updated date on entry hover 2025-01-10 01:43:34 -06:00
33f42adb11 Merge remote-tracking branch 'origin/master' 2025-01-09 22:09:01 -06:00
918ec1bde3 cores rework (#505)
This PR changes the core count to be defined in the device instead of
being a const value.
This is mostly a change for future features I want to implement and
should not impact any functionality.
The console will now log the range of cores requested from the
application, and for now, if the requested range is not 0 to 2 (the 3
cores used for application emulation), it will give an error message
which tells the user to contact me on discord. I'm doing this because
I'm interested in finding applications/games that don't use 3 cores and
the error will be removed in the future once I've gotten enough data.
2025-01-09 21:43:18 -06:00
cca429d46a misc: chore: restore not enable 2025-01-09 21:42:54 -06:00
130 changed files with 5737 additions and 4772 deletions

View File

@ -22,7 +22,7 @@ body:
id: log
attributes:
label: Log file
description: A log file will help our developers to better diagnose and fix the issue.
description: "A log file will help our developers to better diagnose and fix the issue. UPLOAD THE FILE. DO NOT COPY AND PASTE THE FILE'S CONTENT."
placeholder: Logs files can be found under "Logs" folder in Ryujinx program folder. They can also be accessed by opening Ryujinx, then going to File > Open Logs Folder. You can drag and drop the log on to the text area (do not copy paste).
validations:
required: true

4
.github/labeler.yml vendored
View File

@ -18,6 +18,10 @@ gpu:
- changed-files:
- any-glob-to-any-file: ['src/Ryujinx.Graphics.Vulkan/**', 'src/Spv.Generator/**']
'graphics-backend:metal':
- changed-files:
- any-glob-to-any-file: ['src/Ryujinx.Graphics.Metal/**', 'src/Ryujinx.Graphics.Metal.SharpMetalExtensions/**']
gui:
- changed-files:
- any-glob-to-any-file: ['src/Ryujinx/**', 'src/Ryujinx.UI.Common/**', 'src/Ryujinx.UI.LocaleGenerator/**']

View File

@ -129,11 +129,11 @@ jobs:
with:
global-json-file: global.json
- name: Setup LLVM 14
- name: Setup LLVM 17
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 14
sudo ./llvm.sh 17
- name: Install rcodesign
run: |

View File

@ -202,7 +202,7 @@ jobs:
macos_release:
name: Release MacOS universal
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
@ -210,11 +210,11 @@ jobs:
with:
global-json-file: global.json
- name: Setup LLVM 15
- name: Setup LLVM 17
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 15
sudo ./llvm.sh 17
- name: Install rcodesign
run: |

View File

@ -3,16 +3,6 @@ name: Release job
on:
workflow_dispatch:
inputs: {}
push:
branches: [ release ]
paths-ignore:
- '.github/**'
- 'docs/**'
- 'assets/**'
- '*.yml'
- '*.json'
- '*.config'
- '*.md'
concurrency: release
@ -193,7 +183,7 @@ jobs:
macos_release:
name: Release MacOS universal
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
@ -201,11 +191,11 @@ jobs:
with:
global-json-file: global.json
- name: Setup LLVM 15
- name: Setup LLVM 17
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 15
sudo ./llvm.sh 17
- name: Install rcodesign
run: |

View File

@ -3,13 +3,13 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia" Version="11.0.10" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.0.10" />
<PackageVersion Include="Avalonia.Desktop" Version="11.0.10" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.0.10" />
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.0.10" />
<PackageVersion Include="Avalonia.Svg" Version="11.0.0.18" />
<PackageVersion Include="Avalonia.Svg.Skia" Version="11.0.0.18" />
<PackageVersion Include="Avalonia" Version="11.0.13" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.0.13" />
<PackageVersion Include="Avalonia.Desktop" Version="11.0.13" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.0.13" />
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.0.13" />
<PackageVersion Include="Avalonia.Svg" Version="11.0.0.19" />
<PackageVersion Include="Avalonia.Svg.Skia" Version="11.0.0.19" />
<PackageVersion Include="Microsoft.Build.Framework" Version="17.11.4" />
<PackageVersion Include="Microsoft.Build.Utilities.Core" Version="17.12.6" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
@ -18,7 +18,7 @@
<PackageVersion Include="Projektanker.Icons.Avalonia.MaterialDesign" Version="9.4.0"/>
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0"/>
<PackageVersion Include="Concentus" Version="2.2.0" />
<PackageVersion Include="Concentus" Version="2.2.2" />
<PackageVersion Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageVersion Include="DynamicData" Version="9.0.4" />
<PackageVersion Include="FluentAvaloniaUI" Version="2.0.5" />
@ -26,7 +26,7 @@
<PackageVersion Include="LibHac" Version="0.19.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.1.2" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.3.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
<PackageVersion Include="MsgPack.Cli" Version="1.0.1" />
@ -42,17 +42,17 @@
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies" Version="5.0.3-build14" />
<PackageVersion Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Version="1.2.0" />
<PackageVersion Include="Ryujinx.SDL2-CS" Version="2.30.0-build32" />
<PackageVersion Include="Gommon" Version="2.7.0.1" />
<PackageVersion Include="Gommon" Version="2.7.0.2" />
<PackageVersion Include="securifybv.ShellLink" Version="0.1.0" />
<PackageVersion Include="Sep" Version="0.6.0" />
<PackageVersion Include="shaderc.net" Version="0.1.0" />
<PackageVersion Include="SharpMetal" Version="1.0.0-preview21" />
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
<PackageVersion Include="Silk.NET.Vulkan" Version="2.21.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.21.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.21.0" />
<PackageVersion Include="SkiaSharp" Version="2.88.7" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.7" />
<PackageVersion Include="Silk.NET.Vulkan" Version="2.22.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.22.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.22.0" />
<PackageVersion Include="SkiaSharp" Version="2.88.9" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
<PackageVersion Include="SPB" Version="0.0.4-build32" />
<PackageVersion Include="System.IO.Hashing" Version="9.0.0" />
<PackageVersion Include="System.Management" Version="9.0.0" />

View File

@ -54,12 +54,13 @@ failing to meet this requirement may result in a poor gameplay experience or une
## Latest build
Stable builds are made every so often onto a separate "release" branch that then gets put into the releases you know and love.
Stable builds are made every so often, based on the `master` branch, that then gets put into the releases you know and love.
These stable builds exist so that the end user can get a more **enjoyable and stable experience**.
They are released every month or so, to ensure consistent updates, while not being an annoying amount of individual updates to download over the course of that month.
You can find the latest stable release [here](https://github.com/GreemDev/Ryujinx/releases/latest).
Canary builds are compiled automatically for each commit on the master branch.
Canary builds are compiled automatically for each commit on the `master` branch.
While we strive to ensure optimal stability and performance prior to pushing an update, these builds **may be unstable or completely broken**.
These canary builds are only recommended for experienced users.
@ -109,7 +110,7 @@ If you are planning to contribute or just want to learn more about this project
- **Configuration**
The emulator has settings for enabling or disabling some logging, remapping controllers, and more.
You can configure all of them through the graphical interface or manually through the config file, `Config.json`, found in the user folder which can be accessed by clicking `Open Ryujinx Folder` under the File menu in the GUI.
You can configure all of them through the graphical interface or manually through the config file, `Config.json`, found in the Ryujinx data folder which can be accessed by clicking `Open Ryujinx Folder` under the File menu in the GUI.
## License

View File

@ -19,7 +19,7 @@ if platform.system() == "Darwin":
else:
OTOOL = shutil.which("llvm-otool")
if OTOOL is None:
for llvm_ver in [15, 14, 13]:
for llvm_ver in [17, 16, 15, 14, 13]:
otool_path = shutil.which(f"llvm-otool-{llvm_ver}")
if otool_path is not None:
OTOOL = otool_path

View File

@ -26,7 +26,7 @@ else:
LIPO = shutil.which("llvm-lipo")
if LIPO is None:
for llvm_ver in [15, 14, 13]:
for llvm_ver in [17, 16, 15, 14, 13]:
lipo_path = shutil.which(f"llvm-lipo-{llvm_ver}")
if lipo_path is not None:
LIPO = lipo_path

View File

@ -67,11 +67,11 @@ python3 "$BASE_DIR/distribution/macos/construct_universal_dylib.py" "$ARM64_APP_
if ! [ -x "$(command -v lipo)" ];
then
if ! [ -x "$(command -v llvm-lipo-14)" ];
if ! [ -x "$(command -v llvm-lipo-17)" ];
then
LIPO=llvm-lipo
else
LIPO=llvm-lipo-14
LIPO=llvm-lipo-17
fi
else
LIPO=lipo

View File

@ -62,11 +62,11 @@ python3 "$BASE_DIR/distribution/macos/construct_universal_dylib.py" "$ARM64_OUTP
if ! [ -x "$(command -v lipo)" ];
then
if ! [ -x "$(command -v llvm-lipo-14)" ];
if ! [ -x "$(command -v llvm-lipo-17)" ];
then
LIPO=llvm-lipo
else
LIPO=llvm-lipo-14
LIPO=llvm-lipo-17
fi
else
LIPO=lipo

View File

@ -19,7 +19,7 @@
<key>CSResourcesFileMapped</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2018 - 2023 Ryujinx Team and Contributors.</string>
<string>Copyright © 2018 - 2025 Ryujinx Team and Contributors.</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.games</string>
<key>LSMinimumSystemVersion</key>

File diff suppressed because it is too large Load Diff

View File

@ -78,5 +78,10 @@ namespace Ryujinx.Common.Configuration.Hid.Controller
/// Controller Rumble Settings
/// </summary>
public RumbleConfigController Rumble { get; set; }
/// <summary>
/// Controller LED Settings
/// </summary>
public LedConfigController Led { get; set; }
}
}

View File

@ -0,0 +1,15 @@
namespace Ryujinx.Common.Configuration.Hid.Controller
{
public class LedConfigController
{
/// <summary>
/// Packed RGB int of the color
/// </summary>
public uint LedColor { get; set; }
/// <summary>
/// Enable LED color changing by the emulator
/// </summary>
public bool EnableLed { get; set; }
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Runtime.InteropServices;
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable InconsistentNaming
namespace Ryujinx.Common.Helper
{
public static class RunningPlatform
{
public static bool IsMacOS => OperatingSystem.IsMacOS();
public static bool IsWindows => OperatingSystem.IsWindows();
public static bool IsLinux => OperatingSystem.IsLinux();
public static bool IsIntelMac => IsMacOS && RuntimeInformation.OSArchitecture is Architecture.X64;
public static bool IsArmMac => IsMacOS && RuntimeInformation.OSArchitecture is Architecture.Arm64;
public static bool IsX64Windows => IsWindows && (RuntimeInformation.OSArchitecture is Architecture.X64);
public static bool IsArmWindows => IsWindows && (RuntimeInformation.OSArchitecture is Architecture.Arm64);
public static bool IsX64Linux => IsLinux && (RuntimeInformation.OSArchitecture is Architecture.X64);
public static bool IsArmLinux => IsLinux && (RuntimeInformation.OSArchitecture is Architecture.Arm64);
}
}

View File

@ -0,0 +1,10 @@
using System;
namespace Ryujinx.Common
{
public class RyujinxException : Exception
{
public RyujinxException(string message) : base(message)
{ }
}
}

View File

@ -1,5 +1,6 @@
using Gommon;
using Gommon;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Helper;
using System;
using System.Linq;
using System.Runtime.InteropServices;
@ -8,19 +9,20 @@ namespace Ryujinx.Common
{
public static class TitleIDs
{
public static ReactiveObject<Optional<string>> CurrentApplication { get; set; } = new();
public static ReactiveObject<Optional<string>> CurrentApplication { get; } = new();
public static GraphicsBackend SelectGraphicsBackend(string titleId, GraphicsBackend currentBackend)
{
switch (currentBackend)
{
case GraphicsBackend.Metal when !OperatingSystem.IsMacOS():
case GraphicsBackend.OpenGl when OperatingSystem.IsMacOS():
return GraphicsBackend.Vulkan;
case GraphicsBackend.Vulkan or GraphicsBackend.OpenGl or GraphicsBackend.Metal:
return currentBackend;
}
if (!(OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture is Architecture.Arm64))
if (!RunningPlatform.IsArmMac)
return GraphicsBackend.Vulkan;
return GreatMetalTitles.ContainsIgnoreCase(titleId) ? GraphicsBackend.Metal : GraphicsBackend.Vulkan;
@ -28,18 +30,28 @@ namespace Ryujinx.Common
public static readonly string[] GreatMetalTitles =
[
"01006f8002326000", // Animal Crossings: New Horizons
"01009bf0072d4000", // Captain Toad: Treasure Tracker
"010076f0049a2000", // Bayonetta
"0100a5c00d162000", // Cuphead
"010023800d64a000", // Deltarune
"01003a30012c0000", // LEGO City Undercover
"010028600EBDA000", // Mario 3D World
"0100152000022000", // Mario Kart 8 Deluxe
"01005CA01580E000", // Persona 5
"0100187003A36000", // Pokémon: Let's Go, Evoli!
"010075a016a3a000", // Persona 4 Arena Ultimax
"0100187003A36000", // Pokémon: Let's Go, Eevee!
"010003f003a34000", // Pokémon: Let's Go, Pikachu!
"01008C0016544000", // Sea of Stars
"01006A800016E000", // Smash Ultimate
"0100000000010000", // Super Mario Odyessy
"01006bb00c6f0000", // The Legend of Zelda: Link's Awakening
// These ones have small issues, but those happen on Vulkan as well:
"01006f8002326000", // Animal Crossings: New Horizons
"01009bf0072d4000", // Captain Toad: Treasure Tracker
"01009510001ca000", // Fast RMX
"01005CA01580E000", // Persona 5 Royale
"0100000000010000", // Super Mario Odyssey
//Isaac claims it has a issue in level 2, but I am not able to replicate it on my M3. More testing would be appreciated:
"010015100b514000", // Super Mario Bros. Wonder
];
public static string GetDiscordGameAsset(string titleId)
@ -47,93 +59,117 @@ namespace Ryujinx.Common
public static readonly string[] DiscordGameAssetKeys =
[
"010055d009f78000", // Fire Emblem: Three Houses
"0100a12011cc8000", // Fire Emblem: Shadow Dragon
//All games are in Alphabetical order by Game name.
//Dragon Quest Franchise
"010008900705c000", // Dragon Quest Builders
"010042000a986000", // Dragon Quest Builders 2
//Fire Emblem Franchise
"0100a6301214e000", // Fire Emblem Engage
"0100a12011cc8000", // Fire Emblem: Shadow Dragon
"010055d009f78000", // Fire Emblem: Three Houses
"0100f15003e64000", // Fire Emblem Warriors
"010071f0143ea000", // Fire Emblem Warriors: Three Hopes
"01007e3006dda000", // Kirby Star Allies
//Kirby Franchise
"01004d300c5ae000", // Kirby and the Forgotten Land
"01006b601380e000", // Kirby's Return to Dream Land Deluxe
"01003fb00c5a8000", // Super Kirby Clash
"0100227010460000", // Kirby Fighters 2
"0100a8e016236000", // Kirby's Dream Buffet
"0100227010460000", // Kirby Fighters 2
"01006b601380e000", // Kirby's Return to Dream Land Deluxe
"01007e3006dda000", // Kirby Star Allies
"01003fb00c5a8000", // Super Kirby Clash
//The Zelda Franchise
"01000b900d8b0000", // Cadence of Hyrule
"0100ae00096ea000", // Hyrule Warriors: Definitive Edition
"01002b00111a2000", // Hyrule Warriors: Age of Calamity
"01007ef00011e000", // The Legend of Zelda: Breath of the Wild
"01006bb00c6f0000", // The Legend of Zelda: Link's Awakening
"01002da013484000", // The Legend of Zelda: Skyward Sword HD
"0100f2c0115b6000", // The Legend of Zelda: Tears of the Kingdom
"01008cf01baac000", // The Legend of Zelda: Echoes of Wisdom
"01000b900d8b0000", // Cadence of Hyrule
"0100ae00096ea000", // Hyrule Warriors: Definitive Edition
"01002b00111a2000", // Hyrule Warriors: Age of Calamity
//Luigi Franchise
"010048701995e000", // Luigi's Mansion 2 HD
"0100dca0064a6000", // Luigi's Mansion 3
//Metroid Franchise
"010093801237c000", // Metroid Dread
"010012101468c000", // Metroid Prime Remastered
"0100000000010000", // SUPER MARIO ODYSSEY
"0100ea80032ea000", // Super Mario Bros. U Deluxe
"01009b90006dc000", // Super Mario Maker 2
"010049900f546000", // Super Mario 3D All-Stars
"010049900F546001", // ^ 64
"010049900F546002", // ^ Sunshine
"010049900F546003", // ^ Galaxy
"010028600ebda000", // Super Mario 3D World + Bowser's Fury
"010015100b514000", // Super Mario Bros. Wonder
"0100152000022000", // Mario Kart 8 Deluxe
"010036b0034e4000", // Super Mario Party
"01006fe013472000", // Mario Party Superstars
"0100965017338000", // Super Mario Party Jamboree
//Monster Hunter Franchise
"0100770008dd8000", // Monster Hunter Generations Ultimate
"0100b04011742000", // Monster Hunter Rise
//Mario Franchise
"01006d0017f7a000", // Mario & Luigi: Brothership
"010003000e146000", // Mario & Sonic at the Olympic Games Tokyo 2020
"010067300059a000", // Mario + Rabbids: Kingdom Battle
"0100317013770000", // Mario + Rabbids: Sparks of Hope
"0100c9c00e25c000", // Mario Golf: Super Rush
"0100152000022000", // Mario Kart 8 Deluxe
"01006fe013472000", // Mario Party Superstars
"010019401051c000", // Mario Strikers: Battle League
"0100bde00862a000", // Mario Tennis Aces
"0100b99019412000", // Mario vs. Donkey Kong
"010049900f546000", // Super Mario 3D All-Stars
"010028600ebda000", // Super Mario 3D World + Bowser's Fury
"010049900F546001", // Super Mario 64
"0100ea80032ea000", // Super Mario Bros. U Deluxe
"010015100b514000", // Super Mario Bros. Wonder
"010049900F546003", // Super Mario Galaxy
"01009b90006dc000", // Super Mario Maker 2
"0100000000010000", // SUPER MARIO ODYSSEY
"010036b0034e4000", // Super Mario Party
"0100965017338000", // Super Mario Party Jamboree
"0100bc0018138000", // Super Mario RPG
"010049900F546002", // Super Mario Sunshine
"0100a3900c3e2000", // Paper Mario: The Origami King
"0100ecd018ebe000", // Paper Mario: The Thousand-Year Door
"0100bc0018138000", // Super Mario RPG
"0100bde00862a000", // Mario Tennis Aces
"0100c9c00e25c000", // Mario Golf: Super Rush
"010019401051c000", // Mario Strikers: Battle League
"010003000e146000", // Mario & Sonic at the Olympic Games Tokyo 2020
"0100b99019412000", // Mario vs. Donkey Kong
//Pikmin Franchise
"0100aa80194b0000", // Pikmin 1
"0100d680194b2000", // Pikmin 2
"0100f4c009322000", // Pikmin 3 Deluxe
"0100b7c00933a000", // Pikmin 4
"010003f003a34000", // Pokémon: Let's Go Pikachu!
"0100187003a36000", // Pokémon: Let's Go Eevee!
"0100abf008968000", // Pokémon Sword
"01008db008c2c000", // Pokémon Shield
//The Pokémon Franchise
"0100f4300bf2c000", // New Pokémon Snap
"0100000011d90000", // Pokémon Brilliant Diamond
"010018e011d92000", // Pokémon Shining Pearl
"01001f5010dfa000", // Pokémon Legends: Arceus
"01003d200baa2000", // Pokémon Mystery Dungeon - Rescue Team DX
"0100a3d008c5c000", // Pokémon Scarlet
"01008db008c2c000", // Pokémon Shield
"010018e011d92000", // Pokémon Shining Pearl
"0100abf008968000", // Pokémon Sword
"01008f6008c5e000", // Pokémon Violet
"0100b3f000be2000", // Pokkén Tournament DX
"0100f4300bf2c000", // New Pokémon Snap
"0100187003a36000", // Pokémon: Let's Go Eevee!
"010003f003a34000", // Pokémon: Let's Go Pikachu!
"01003bc0000a0000", // Splatoon 2 (US)
//Splatoon Franchise
"0100f8f0000a2000", // Splatoon 2 (EU)
"01003c700009c000", // Splatoon 2 (JP)
"01003bc0000a0000", // Splatoon 2 (US)
"0100c2500fc20000", // Splatoon 3
"0100ba0018500000", // Splatoon 3: Splatfest World Premiere
"010040600c5ce000", // Tetris 99
"0100277011f1a000", // Super Mario Bros. 35
"0100ad9012510000", // PAC-MAN 99
//NSO Membership games
"0100ccf019c8c000", // F-ZERO 99
"0100d870045b6000", // NES - Nintendo Switch Online
"01008d300c50c000", // SNES - Nintendo Switch Online
"0100c9a00ece6000", // N64 - Nintendo Switch Online
"0100e0601c632000", // N64 - Nintendo Switch Online 18+
"0100c62011050000", // GB - Nintendo Switch Online
"010012f017576000", // GBA - Nintendo Switch Online
"0100c9a00ece6000", // N64 - Nintendo Switch Online
"0100e0601c632000", // N64 - Nintendo Switch Online 18+
"0100d870045b6000", // NES - Nintendo Switch Online
"0100ad9012510000", // PAC-MAN 99
"010040600c5ce000", // Tetris 99
"01008d300c50c000", // SNES - Nintendo Switch Online
"0100277011f1a000", // Super Mario Bros. 35
//Misc Nintendo 1st party games
"01000320000cc000", // 1-2 Switch
"0100300012f2a000", // Advance Wars 1+2: Re-Boot Camp
"01006f8002326000", // Animal Crossing: New Horizons
@ -148,33 +184,44 @@ namespace Ryujinx.Common
"01006a800016e000", // Super Smash Bros. Ultimate
"0100a9400c9c2000", // Tokyo Mirage Sessions #FE Encore
//Bayonetta Franchise
"010076f0049a2000", // Bayonetta
"01007960049a0000", // Bayonetta 2
"01004a4010fea000", // Bayonetta 3
"0100cf5010fec000", // Bayonetta Origins: Cereza and the Lost Demon
//Persona Franchise
"0100dcd01525a000", // Persona 3 Portable
"010062b01525c000", // Persona 4 Golden
"010075a016a3a000", // Persona 4 Arena Ultimax
"010062b01525c000", // Persona 4 Golden
"01005ca01580e000", // Persona 5 Royal
"0100801011c3e000", // Persona 5 Strikers
"010087701b092000", // Persona 5 Tactica
"01009aa000faa000", // Sonic Mania
//Sonic Franchise
"01004ad014bf0000", // Sonic Frontiers
"01009aa000faa000", // Sonic Mania
"01005ea01c0fc000", // SONIC X SHADOW GENERATIONS
"01005ea01c0fc001", // ^
//Xenoblade Franchise
"0100ff500e34a000", // Xenoblade Chronicles - Definitive Edition
"0100e95004038000", // Xenoblade Chronicles 2
"010074f013262000", // Xenoblade Chronicles 3
//Misc Games
"010056e00853a000", // A Hat in Time
"0100fd1014726000", // Baldurs Gate: Dark Alliance
"0100dbf01000a000", // Burnout Paradise Remastered
"0100744001588000", // Cars 3: Driven to Win
"0100b41013c82000", // Cruis'n Blast
"010085900337e000", // Death Squared
"01001b300b9be000", // Diablo III: Eternal Collection
"01008c8012920000", // Dying Light Platinum Edition
"01001cc01b2d4000", // Goat Simulator 3
"01003620068ea000", // Hand of Fate 2
"010085500130a000", // Lego City: Undercover
"010073c01af34000", // LEGO Horizon Adventures
"0100770008dd8000", // Monster Hunter Generations Ultimate
"0100b04011742000", // Monster Hunter Rise
"0100853015e86000", // No Man's Sky
"01007bb017812000", // Portal
"0100abd01785c000", // Portal 2
@ -190,6 +237,8 @@ namespace Ryujinx.Common
"01000a10041ea000", // The Elder Scrolls V: Skyrim
"010057a01e4d4000", // TSUKIHIME -A piece of blue glass moon-
"010080b00ad66000", // Undertale
"010069401adb8000", // Unicorn Overlord
"0100534009ff2000", // Yonder - The cloud catcher chronicles
];
}
}

View File

@ -118,7 +118,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
private static string GetDiskCachePath()
{
return GraphicsConfig.EnableShaderCache && GraphicsConfig.TitleId != null
? Path.Combine(AppDataManager.GamesDirPath, GraphicsConfig.TitleId, "cache", "shader")
? Path.Combine(AppDataManager.GamesDirPath, GraphicsConfig.TitleId.ToLower(), "cache", "shader")
: null;
}

View File

@ -92,7 +92,7 @@ namespace Ryujinx.Graphics.Vulkan
DriverId.MesaDozen => "Dozen",
DriverId.MesaNvk => "NVK",
DriverId.ImaginationOpenSourceMesa => "Imagination (Open)",
DriverId.MesaAgxv => "Honeykrisp",
DriverId.MesaHoneykrisp => "Honeykrisp",
_ => id.ToString(),
};
}

View File

@ -710,9 +710,8 @@ namespace Ryujinx.HLE.FileSystem
{
updateNcasItem.Add((nca.Header.ContentType, entry.FullName));
}
else
else if (updateNcas.TryAdd(nca.Header.TitleId, new List<(NcaContentType, string)>()))
{
updateNcas.Add(nca.Header.TitleId, new List<(NcaContentType, string)>());
updateNcas[nca.Header.TitleId].Add((nca.Header.ContentType, entry.FullName));
}
}
@ -898,9 +897,8 @@ namespace Ryujinx.HLE.FileSystem
{
updateNcasItem.Add((nca.Header.ContentType, entry.FullPath));
}
else
else if (updateNcas.TryAdd(nca.Header.TitleId, new List<(NcaContentType, string)>()))
{
updateNcas.Add(nca.Header.TitleId, new List<(NcaContentType, string)>());
updateNcas[nca.Header.TitleId].Add((nca.Header.ContentType, entry.FullPath));
}

View File

@ -26,10 +26,20 @@ namespace Ryujinx.HLE.HOS.Applets
{
_normalSession = normalSession;
_interactiveSession = interactiveSession;
// TODO(jduncanator): Parse PlayerSelectConfig from input data
_normalSession.Push(BuildResponse());
UserProfile selected = _system.Device.UIHandler.ShowPlayerSelectDialog();
if (selected == null)
{
_normalSession.Push(BuildResponse());
}
else if (selected.UserId == new UserId("00000000000000000000000000000080"))
{
_normalSession.Push(BuildGuestResponse());
}
else
{
_normalSession.Push(BuildResponse(selected));
}
AppletStateChanged?.Invoke(this, null);
_system.ReturnFocus();
@ -37,16 +47,34 @@ namespace Ryujinx.HLE.HOS.Applets
return ResultCode.Success;
}
private byte[] BuildResponse()
private byte[] BuildResponse(UserProfile selectedUser)
{
UserProfile currentUser = _system.AccountManager.LastOpenedUser;
using MemoryStream stream = MemoryStreamManager.Shared.GetStream();
using BinaryWriter writer = new(stream);
writer.Write((ulong)PlayerSelectResult.Success);
currentUser.UserId.Write(writer);
selectedUser.UserId.Write(writer);
return stream.ToArray();
}
private byte[] BuildGuestResponse()
{
using MemoryStream stream = MemoryStreamManager.Shared.GetStream();
using BinaryWriter writer = new(stream);
writer.Write(new byte());
return stream.ToArray();
}
private byte[] BuildResponse()
{
using MemoryStream stream = MemoryStreamManager.Shared.GetStream();
using BinaryWriter writer = new(stream);
writer.Write((ulong)PlayerSelectResult.Failure);
return stream.ToArray();
}

View File

@ -284,7 +284,7 @@ namespace Ryujinx.HLE.HOS
ProcessCreationInfo creationInfo = new("Service", 1, 0, 0x8000000, 1, Flags, 0, 0);
uint[] defaultCapabilities = {
0x030363F7,
(((uint)KScheduler.CpuCoresCount - 1) << 24) + (((uint)KScheduler.CpuCoresCount - 1) << 16) + 0x63F7u,
0x1FFFFFCF,
0x207FFFEF,
0x47E0060F,

View File

@ -63,6 +63,7 @@ namespace Ryujinx.HLE.HOS.Kernel
TickSource = tickSource;
Device = device;
Memory = memory;
KScheduler.CpuCoresCount = device.CpuCoresCount;
Running = true;

View File

@ -37,7 +37,7 @@ namespace Ryujinx.HLE.HOS.Kernel
return result;
}
process.DefaultCpuCore = 3;
process.DefaultCpuCore = KScheduler.CpuCoresCount - 1;
context.Processes.TryAdd(process.Pid, process);

View File

@ -277,7 +277,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
return result;
}
result = Capabilities.InitializeForUser(capabilities, MemoryManager);
result = Capabilities.InitializeForUser(capabilities, MemoryManager, IsApplication);
if (result != Result.Success)
{

View File

@ -35,15 +35,15 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
DebuggingFlags &= ~3u;
KernelReleaseVersion = KProcess.KernelVersionPacked;
return Parse(capabilities, memoryManager);
return Parse(capabilities, memoryManager, false);
}
public Result InitializeForUser(ReadOnlySpan<uint> capabilities, KPageTableBase memoryManager)
public Result InitializeForUser(ReadOnlySpan<uint> capabilities, KPageTableBase memoryManager, bool isApplication)
{
return Parse(capabilities, memoryManager);
return Parse(capabilities, memoryManager, isApplication);
}
private Result Parse(ReadOnlySpan<uint> capabilities, KPageTableBase memoryManager)
private Result Parse(ReadOnlySpan<uint> capabilities, KPageTableBase memoryManager, bool isApplication)
{
int mask0 = 0;
int mask1 = 0;
@ -54,7 +54,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
if (cap.GetCapabilityType() != CapabilityType.MapRange)
{
Result result = ParseCapability(cap, ref mask0, ref mask1, memoryManager);
Result result = ParseCapability(cap, ref mask0, ref mask1, memoryManager, isApplication);
if (result != Result.Success)
{
@ -120,7 +120,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
return Result.Success;
}
private Result ParseCapability(uint cap, ref int mask0, ref int mask1, KPageTableBase memoryManager)
private Result ParseCapability(uint cap, ref int mask0, ref int mask1, KPageTableBase memoryManager, bool isApplication)
{
CapabilityType code = cap.GetCapabilityType();
@ -176,6 +176,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
AllowedCpuCoresMask = GetMaskFromMinMax(lowestCpuCore, highestCpuCore);
AllowedThreadPriosMask = GetMaskFromMinMax(lowestThreadPrio, highestThreadPrio);
if (isApplication)
Ryujinx.Common.Logging.Logger.Info?.Print(Ryujinx.Common.Logging.LogClass.Application, $"Application requested cores with index range {lowestCpuCore} to {highestCpuCore}");
break;
}

View File

@ -2683,7 +2683,7 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall
return KernelResult.InvalidCombination;
}
if ((uint)preferredCore > 3)
if ((uint)preferredCore > KScheduler.CpuCoresCount - 1)
{
if ((preferredCore | 2) != -1)
{

View File

@ -9,13 +9,11 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
partial class KScheduler : IDisposable
{
public const int PrioritiesCount = 64;
public const int CpuCoresCount = 4;
public static int CpuCoresCount;
private const int RoundRobinTimeQuantumMs = 10;
private static readonly int[] _preemptionPriorities = { 59, 59, 59, 63 };
private static readonly int[] _srcCoresHighestPrioThreads = new int[CpuCoresCount];
private static int[] _srcCoresHighestPrioThreads;
private readonly KernelContext _context;
private readonly int _coreId;
@ -47,6 +45,16 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
_coreId = coreId;
_currentThread = null;
if (_srcCoresHighestPrioThreads == null)
{
_srcCoresHighestPrioThreads = new int[CpuCoresCount];
}
}
private static int PreemptionPriorities(int index)
{
return index == CpuCoresCount - 1 ? 63 : 59;
}
public static ulong SelectThreads(KernelContext context)
@ -437,7 +445,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
for (int core = 0; core < CpuCoresCount; core++)
{
RotateScheduledQueue(context, core, _preemptionPriorities[core]);
RotateScheduledQueue(context, core, PreemptionPriorities(core));
}
context.CriticalSection.Leave();

View File

@ -56,7 +56,7 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc
ProfilesJson profilesJson = JsonHelper.DeserializeFromFile(_profilesJsonPath, _serializerContext.ProfilesJson);
return profilesJson.Profiles
.FindFirst(profile => profile.AccountState == AccountState.Open)
.FindFirst(profile => profile.UserId == profilesJson.LastOpened)
.Convert(profileJson => new UserProfile(new UserId(profileJson.UserId), profileJson.Name,
profileJson.Image, profileJson.LastModifiedTimestamp));
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -659,7 +659,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
if (string.IsNullOrWhiteSpace(filePath))
{
throw new InvalidSystemResourceException("JIT (010000000000003B) system title not found! The JIT will not work, provide the system archive to fix this error. (See https://github.com/GreemDev/Ryujinx#requirements for more information)");
throw new InvalidSystemResourceException("JIT (010000000000003B) system title not found! The JIT will not work, provide the system archive to fix this error. (See https://github.com/Ryubing/Ryujinx#requirements for more information)");
}
context.Device.LoadNca(filePath);

View File

@ -702,6 +702,18 @@ namespace Ryujinx.HLE.HOS.Services.Hid
return ResultCode.Success;
}
[CommandCmif(92)]
// SetGestureOutputRanges(pid, ushort Unknown0)
public ResultCode SetGestureOutputRanges(ServiceCtx context)
{
ulong pid = context.Request.HandleDesc.PId;
ushort unknown0 = context.RequestData.ReadUInt16();
Logger.Stub?.PrintStub(LogClass.ServiceHid, new { pid, unknown0 });
return ResultCode.Success;
}
[CommandCmif(100)]
// SetSupportedNpadStyleSet(pid, nn::applet::AppletResourceUserId, nn::hid::NpadStyleTag)

View File

@ -105,7 +105,7 @@ namespace Ryujinx.HLE.HOS.Services.Sdb.Pl
titleName = "Unknown";
}
throw new InvalidSystemResourceException($"{titleName} ({fontTitle:x8}) system title not found! This font will not work, provide the system archive to fix this error. (See https://github.com/GreemDev/Ryujinx#requirements for more information)");
throw new InvalidSystemResourceException($"{titleName} ({fontTitle:x8}) system title not found! This font will not work, provide the system archive to fix this error. (See https://github.com/Ryubing/Ryujinx#requirements for more information)");
}
}
else

View File

@ -24,14 +24,14 @@ namespace Ryujinx.HLE.HOS.Services
// not large enough.
private const int PointerBufferSize = 0x8000;
private readonly static uint[] _defaultCapabilities = {
0x030363F7,
private static uint[] _defaultCapabilities => [
(((uint)KScheduler.CpuCoresCount - 1) << 24) + (((uint)KScheduler.CpuCoresCount - 1) << 16) + 0x63F7u,
0x1FFFFFCF,
0x207FFFEF,
0x47E0060F,
0x0048BFFF,
0x01007FFF,
};
];
// The amount of time Dispose() will wait to Join() the thread executing the ServerLoop()
private static readonly TimeSpan _threadJoinTimeout = TimeSpan.FromSeconds(3);

View File

@ -244,6 +244,15 @@ namespace Ryujinx.HLE.HOS.Services.Settings
return ResultCode.Success;
}
[CommandCmif(68)]
// GetSerialNumber() -> buffer<nn::settings::system::SerialNumber, 0x16>
public ResultCode GetSerialNumber(ServiceCtx context)
{
context.ResponseData.Write(Encoding.ASCII.GetBytes("RYU00000000000"));
return ResultCode.Success;
}
[CommandCmif(77)]
// GetDeviceNickName() -> buffer<nn::settings::system::DeviceNickName, 0x16>
public ResultCode GetDeviceNickName(ServiceCtx context)

View File

@ -51,6 +51,9 @@ namespace Ryujinx.HLE.HOS.Services.Spl
context.ResponseData.Write(configValue);
if (result == SmcResult.Success)
return ResultCode.Success;
return (ResultCode)((int)result << 9) | ResultCode.ModuleId;
}

View File

@ -26,7 +26,17 @@ namespace Ryujinx.HLE.Loaders.Processes
private ulong _latestPid;
public ProcessResult ActiveApplication => _processesByPid[_latestPid];
public ProcessResult ActiveApplication
{
get
{
if (!_processesByPid.TryGetValue(_latestPid, out ProcessResult value))
throw new RyujinxException(
$"The HLE Process map did not have a process with ID {_latestPid}. Are you missing firmware?");
return value;
}
}
public ProcessLoader(Switch device)
{

View File

@ -161,5 +161,20 @@ namespace Ryujinx.HLE
{
return 1000 / _frameRate[FrameTypeGame];
}
public string FormatGameFrameRate()
{
double frameRate = GetGameFrameRate();
double frameTime = GetGameFrameTime();
return $"{frameRate:00.00} FPS ({frameTime:00.00}ms)";
}
public string FormatFifoPercent()
{
double fifoPercent = GetFifoPercent();
return $"FIFO: {fifoPercent:00.00}%";
}
}
}

View File

@ -48,6 +48,7 @@
<EmbeddedResource Include="HOS\Applets\SoftwareKeyboard\Resources\Icon_BtnB.png" />
<EmbeddedResource Include="HOS\Applets\SoftwareKeyboard\Resources\Icon_KeyF6.png" />
<EmbeddedResource Include="HOS\Services\Account\Acc\DefaultUserImage.jpg" />
<EmbeddedResource Include="HOS\Services\Account\Acc\GuestUserImage.jpg" />
</ItemGroup>
</Project>

View File

@ -32,8 +32,10 @@ namespace Ryujinx.HLE
public TamperMachine TamperMachine { get; }
public IHostUIHandler UIHandler { get; }
public VSyncMode VSyncMode { get; set; } = VSyncMode.Switch;
public bool CustomVSyncIntervalEnabled { get; set; } = false;
public int CpuCoresCount = 4; //Switch 1 has 4 cores
public VSyncMode VSyncMode { get; set; }
public bool CustomVSyncIntervalEnabled { get; set; }
public int CustomVSyncInterval { get; set; }
public long TargetVSyncInterval { get; set; } = 60;

View File

@ -1,4 +1,5 @@
using Ryujinx.HLE.HOS.Applets;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
namespace Ryujinx.HLE.UI
@ -59,5 +60,11 @@ namespace Ryujinx.HLE.UI
/// Gets fonts and colors used by the host.
/// </summary>
IHostUITheme HostUITheme { get; }
/// <summary>
/// Displays the player select dialog and returns the selected profile.
/// </summary>
UserProfile ShowPlayerSelectDialog();
}
}

View File

@ -1,6 +1,7 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Logging;
using SDL2;
using System;
using System.Collections.Generic;
using System.Numerics;
@ -118,6 +119,11 @@ namespace Ryujinx.Input.SDL2
result |= GamepadFeaturesFlag.Rumble;
}
if (SDL_GameControllerHasLED(_gamepadHandle) == SDL_bool.SDL_TRUE)
{
result |= GamepadFeaturesFlag.Led;
}
return result;
}

View File

@ -24,5 +24,10 @@ namespace Ryujinx.Input
/// <remarks>Also named sixaxis</remarks>
/// </summary>
Motion,
/// <summary>
/// The LED on the back of modern PlayStation controllers (DualSense &amp; DualShock 4).
/// </summary>
Led,
}
}

View File

@ -1,36 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>
<None Remove="Resources\Controller_JoyConLeft.svg" />
<None Remove="Resources\Controller_JoyConPair.svg" />
<None Remove="Resources\Controller_JoyConRight.svg" />
<None Remove="Resources\Controller_ProCon.svg" />
<None Remove="Resources\Icon_NCA.png" />
<None Remove="Resources\Icon_NRO.png" />
<None Remove="Resources\Icon_NSO.png" />
<None Remove="Resources\Icon_NSP.png" />
<None Remove="Resources\Icon_XCI.png" />
<None Remove="Resources\Logo_Amiibo.png" />
<None Remove="Resources\Logo_Discord.png" />
<None Remove="Resources\Logo_GitHub.png" />
<None Remove="Resources\Logo_Ryujinx.png" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="DiscordRichPresence" />
<PackageReference Include="DynamicData" />
<PackageReference Include="securifybv.ShellLink" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
<ProjectReference Include="..\Ryujinx.HLE\Ryujinx.HLE.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.Vulkan\Ryujinx.Graphics.Vulkan.csproj" />
</ItemGroup>
</Project>

View File

@ -489,7 +489,7 @@ namespace Ryujinx.Ava
Dispatcher.UIThread.InvokeAsync(() =>
{
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device.Processes.ActiveApplication, Program.Version);
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowTitleBar);
});
_viewModel.SetUiProgressHandlers(Device);
@ -674,7 +674,7 @@ namespace Ryujinx.Ava
public async Task<bool> LoadGuestApplication(BlitStruct<ApplicationControlProperty>? customNacpData = null)
{
InitializeSwitchInstance();
InitEmulatedSwitch();
MainWindow.UpdateGraphicsConfig();
SystemVersion firmwareVersion = ContentManager.GetCurrentFirmwareVersion();
@ -757,6 +757,8 @@ namespace Ryujinx.Ava
{
romFsFiles = Directory.GetFiles(ApplicationPath, "*.romfs");
}
Logger.Notice.Print(LogClass.Application, $"Loading unpacked content archive from '{ApplicationPath}'.");
if (romFsFiles.Length > 0)
{
@ -783,6 +785,8 @@ namespace Ryujinx.Ava
}
else if (File.Exists(ApplicationPath))
{
Logger.Notice.Print(LogClass.Application, $"Loading content archive from '{ApplicationPath}'.");
switch (Path.GetExtension(ApplicationPath).ToLowerInvariant())
{
case ".xci":
@ -872,7 +876,7 @@ namespace Ryujinx.Ava
Device?.System.TogglePauseEmulation(false);
_viewModel.IsPaused = false;
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version);
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowTitleBar);
Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed");
}
@ -881,11 +885,11 @@ namespace Ryujinx.Ava
Device?.System.TogglePauseEmulation(true);
_viewModel.IsPaused = true;
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, LocaleManager.Instance[LocaleKeys.Paused]);
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowTitleBar, LocaleManager.Instance[LocaleKeys.Paused]);
Logger.Info?.Print(LogClass.Emulation, "Emulation was paused");
}
private void InitializeSwitchInstance()
private void InitEmulatedSwitch()
{
// Initialize KeySet.
VirtualFileSystem.ReloadKeySet();
@ -1040,7 +1044,7 @@ namespace Ryujinx.Ava
_viewModel.WindowState = WindowState.FullScreen;
}
if (_viewModel.WindowState is WindowState.FullScreen)
if (_viewModel.WindowState is WindowState.FullScreen || _viewModel.StartGamesWithoutUI)
{
_viewModel.ShowMenuAndStatusBar = false;
}
@ -1147,8 +1151,8 @@ namespace Ryujinx.Ava
LocaleManager.Instance[LocaleKeys.VolumeShort] + $": {(int)(Device.GetVolume() * 100)}%",
dockedMode,
ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(),
$"{Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)",
$"FIFO: {Device.Statistics.GetFifoPercent():00.00} %",
Device.Statistics.FormatGameFrameRate(),
Device.Statistics.FormatFifoPercent(),
_displayCount));
}

View File

@ -0,0 +1,13 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style Selector="MenuItem.withCheckbox Viewbox#PART_IconPresenter">
<Setter Property="MaxHeight" Value="36" />
<Setter Property="MinHeight" Value="36" />
<Setter Property="MaxWidth" Value="36" />
<Setter Property="MinWidth" Value="36" />
</Style>
<Style Selector="MenuItem.withCheckbox ContentPresenter#PART_HeaderPresenter">
<Setter Property="Padding" Value="-10,0,0,0" />
</Style>
</Styles>

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
using Avalonia.Controls.Notifications;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using Gommon;
using LibHac;
using LibHac.Account;
using LibHac.Common;
@ -15,6 +15,7 @@ using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.Utilities;
using Ryujinx.Ava.Utilities.Configuration;
using Ryujinx.Common.Helper;
using Ryujinx.Common.Logging;
@ -295,22 +296,150 @@ namespace Ryujinx.Ava.Common
};
extractorThread.Start();
}
public static void ExtractAoc(string destination, string updateFilePath, string updateName)
{
var cancellationToken = new CancellationTokenSource();
UpdateWaitWindow waitingDialog = new(
RyujinxApp.FormatTitle(LocaleKeys.DialogNcaExtractionTitle),
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogNcaExtractionMessage, NcaSectionType.Data, Path.GetFileName(updateFilePath)),
cancellationToken);
Thread extractorThread = new(() =>
{
Dispatcher.UIThread.Post(waitingDialog.Show);
using FileStream file = new(updateFilePath, FileMode.Open, FileAccess.Read);
Nca publicDataNca = null;
string extension = Path.GetExtension(updateFilePath).ToLower();
if (extension is ".nsp")
{
var pfsTemp = new PartitionFileSystem();
pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
IFileSystem pfs = pfsTemp;
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
{
using var ncaFile = new UniqueRef<IFile>();
pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage());
if (nca.Header.ContentType is NcaContentType.PublicData && nca.SectionExists(NcaSectionType.Data))
{
publicDataNca = nca;
}
}
}
if (publicDataNca is null)
{
Logger.Error?.Print(LogClass.Application, "Extraction failure. The NCA was not present in the selected file");
Dispatcher.UIThread.InvokeAsync(async () =>
{
waitingDialog.Close();
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogNcaExtractionMainNcaNotFoundErrorMessage]);
});
return;
}
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
? IntegrityCheckLevel.ErrorOnInvalid
: IntegrityCheckLevel.None;
int index = Nca.GetSectionIndexFromType(NcaSectionType.Data, publicDataNca.Header.ContentType);
try
{
IFileSystem ncaFileSystem = publicDataNca.OpenFileSystem(index, IntegrityCheckLevel.ErrorOnInvalid);
FileSystemClient fsClient = _horizonClient.Fs;
string source = DateTime.Now.ToFileTime().ToString()[10..];
string output = DateTime.Now.ToFileTime().ToString()[10..];
using var uniqueSourceFs = new UniqueRef<IFileSystem>(ncaFileSystem);
using var uniqueOutputFs = new UniqueRef<IFileSystem>(new LocalFileSystem(destination));
fsClient.Register(source.ToU8Span(), ref uniqueSourceFs.Ref);
fsClient.Register(output.ToU8Span(), ref uniqueOutputFs.Ref);
(Result? resultCode, bool canceled) = CopyDirectory(fsClient, $"{source}:/", $"{output}:/", cancellationToken.Token);
if (!canceled)
{
if (resultCode.Value.IsFailure())
{
Logger.Error?.Print(LogClass.Application, $"LibHac returned error code: {resultCode.Value.ErrorCode}");
Dispatcher.UIThread.InvokeAsync(async () =>
{
waitingDialog.Close();
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogNcaExtractionCheckLogErrorMessage]);
});
}
else if (resultCode.Value.IsSuccess())
{
Dispatcher.UIThread.Post(waitingDialog.Close);
NotificationHelper.ShowInformation(
RyujinxApp.FormatTitle(LocaleKeys.DialogNcaExtractionTitle),
$"{updateName}\n\n{LocaleManager.Instance[LocaleKeys.DialogNcaExtractionSuccessMessage]}");
}
}
fsClient.Unmount(source.ToU8Span());
fsClient.Unmount(output.ToU8Span());
}
catch (ArgumentException ex)
{
Logger.Error?.Print(LogClass.Application, $"{ex.Message}");
Dispatcher.UIThread.InvokeAsync(async () =>
{
waitingDialog.Close();
await ContentDialogHelper.CreateErrorDialog(ex.Message);
});
}
})
{
Name = "GUI.AocExtractorThread",
IsBackground = true,
};
extractorThread.Start();
}
public static async Task ExtractAoc(IStorageProvider storageProvider, string updateFilePath, string updateName)
{
Optional<IStorageFolder> result = await storageProvider.OpenSingleFolderPickerAsync(new FolderPickerOpenOptions
{
Title = LocaleManager.Instance[LocaleKeys.FolderDialogExtractTitle]
});
if (!result.HasValue) return;
ExtractAoc(result.Value.Path.LocalPath, updateFilePath, updateName);
}
public static async Task ExtractSection(IStorageProvider storageProvider, NcaSectionType ncaSectionType, string titleFilePath, string titleName, int programIndex = 0)
{
var result = await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
Optional<IStorageFolder> result = await storageProvider.OpenSingleFolderPickerAsync(new FolderPickerOpenOptions
{
Title = LocaleManager.Instance[LocaleKeys.FolderDialogExtractTitle],
AllowMultiple = false,
Title = LocaleManager.Instance[LocaleKeys.FolderDialogExtractTitle]
});
if (result.Count == 0)
{
return;
}
if (!result.HasValue) return;
ExtractSection(result[0].Path.LocalPath, ncaSectionType, titleFilePath, titleName, programIndex);
ExtractSection(result.Value.Path.LocalPath, ncaSectionType, titleFilePath, titleName, programIndex);
}
public static (Result? result, bool canceled) CopyDirectory(FileSystemClient fs, string sourcePath, string destPath, CancellationToken token)

View File

@ -6,7 +6,7 @@ namespace Ryujinx.Ava.Common.Models
public bool IsBundled { get; } = System.IO.Path.GetExtension(ContainerPath)?.ToLower() == ".xci";
public string FileName => System.IO.Path.GetFileName(ContainerPath);
public string TitleIdStr => TitleId.ToString("x16");
public string TitleIdStr => TitleId.ToString("x16").ToUpper();
public ulong TitleIdBase => TitleId & ~0x1FFFUL;
}
}

View File

@ -4,11 +4,11 @@ namespace Ryujinx.Ava.Common
{
public static class ThemeManager
{
public static event EventHandler ThemeChanged;
public static event Action ThemeChanged;
public static void OnThemeChanged()
{
ThemeChanged?.Invoke(null, EventArgs.Empty);
ThemeChanged?.Invoke();
}
}
}

View File

@ -1,6 +1,8 @@
using DiscordRPC;
using Gommon;
using Humanizer;
using Humanizer.Localisation;
using Ryujinx.Ava.Utilities;
using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Ava.Utilities.Configuration;
using Ryujinx.Common;
@ -45,16 +47,7 @@ namespace Ryujinx.Ava
};
ConfigurationState.Instance.EnableDiscordIntegration.Event += Update;
TitleIDs.CurrentApplication.Event += (_, e) =>
{
if (e.NewValue)
SwitchToPlayingState(
ApplicationLibrary.LoadAndSaveMetaData(e.NewValue),
Switch.Shared.Processes.ActiveApplication
);
else
SwitchToMainState();
};
TitleIDs.CurrentApplication.Event += (_, e) => Use(e.NewValue);
}
private static void Update(object sender, ReactiveEventArgs<bool> evnt)
@ -75,11 +68,23 @@ namespace Ryujinx.Ava
_discordClient = new DiscordRpcClient(ApplicationId);
_discordClient.Initialize();
_discordClient.SetPresence(_discordPresenceMain);
Use(TitleIDs.CurrentApplication);
}
}
}
public static void Use(Optional<string> titleId)
{
if (titleId.TryGet(out string tid))
SwitchToPlayingState(
ApplicationLibrary.LoadAndSaveMetaData(tid),
Switch.Shared.Processes.ActiveApplication
);
else
SwitchToMainState();
}
private static void SwitchToPlayingState(ApplicationMetadata appMeta, ProcessResult procRes)
{
_discordClient?.SetPresence(new RichPresence
@ -93,7 +98,7 @@ namespace Ryujinx.Ava
},
Details = TruncateToByteLength($"Playing {appMeta.Title}"),
State = appMeta.LastPlayed.HasValue && appMeta.TimePlayed.TotalSeconds > 5
? $"Total play time: {appMeta.TimePlayed.Humanize(2, false, maxUnit: TimeUnit.Hour)}"
? $"Total play time: {ValueFormatUtils.FormatTimeSpan(appMeta.TimePlayed)}"
: "Never played",
Timestamps = Timestamps.Now
});

View File

@ -1,4 +1,4 @@
using DiscordRPC;
using DiscordRPC;
using LibHac.Tools.FsSystem;
using Ryujinx.Audio.Backends.SDL2;
using Ryujinx.Ava;

View File

@ -228,8 +228,6 @@ namespace Ryujinx.Headless
_inputConfiguration ??= [];
_enableKeyboard = option.EnableKeyboard;
_enableMouse = option.EnableMouse;
LoadPlayerConfiguration(option.InputProfile1Name, option.InputId1, PlayerIndex.Player1);
LoadPlayerConfiguration(option.InputProfile2Name, option.InputId2, PlayerIndex.Player2);
@ -301,7 +299,10 @@ namespace Ryujinx.Headless
_userChannelPersistence.ShouldRestart = false;
}
_inputManager.Dispose();
try
{
_inputManager.Dispose();
} catch {}
return;
@ -338,12 +339,12 @@ namespace Ryujinx.Headless
{
string label = state switch
{
LoadState => $"PTC : {current}/{total}",
ShaderCacheState => $"Shaders : {current}/{total}",
_ => throw new ArgumentException($"Unknown Progress Handler type {typeof(T)}"),
LoadState => "PTC",
ShaderCacheState => "Shaders",
_ => throw new ArgumentException($"Unknown Progress Handler type {typeof(T)}")
};
Logger.Info?.Print(LogClass.Application, label);
Logger.Info?.Print(LogClass.Application, $"{label} : {current}/{total}");
}
private static WindowBase CreateWindow(Options options)

View File

@ -149,7 +149,7 @@ namespace Ryujinx.Headless
IgnoreMissingServices = configurationState.System.IgnoreMissingServices;
if (NeedsOverride(nameof(IgnoreControllerApplet)))
IgnoreControllerApplet = configurationState.IgnoreApplet;
IgnoreControllerApplet = configurationState.System.IgnoreApplet;
return;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -1,21 +0,0 @@
using System;
namespace Ryujinx.Headless
{
class StatusUpdatedEventArgs(
string vSyncMode,
string dockedMode,
string aspectRatio,
string gameStatus,
string fifoStatus,
string gpuName)
: EventArgs
{
public string VSyncMode = vSyncMode;
public string DockedMode = dockedMode;
public string AspectRatio = aspectRatio;
public string GameStatus = gameStatus;
public string FifoStatus = fifoStatus;
public string GpuName = gpuName;
}
}

View File

@ -26,7 +26,7 @@ namespace Ryujinx.Headless
bool ignoreControllerApplet)
: base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode, ignoreControllerApplet) { }
public override SDL_WindowFlags GetWindowFlags() => SDL_WindowFlags.SDL_WINDOW_METAL;
public override SDL_WindowFlags WindowFlags => SDL_WindowFlags.SDL_WINDOW_METAL;
protected override void InitializeWindowRenderer()
{

View File

@ -108,8 +108,7 @@ namespace Ryujinx.Headless
}
}
}
private readonly GraphicsDebugLevel _glLogLevel;
private SDL2OpenGLContext _openGLContext;
public OpenGLWindow(
@ -121,15 +120,14 @@ namespace Ryujinx.Headless
bool ignoreControllerApplet)
: base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode, ignoreControllerApplet)
{
_glLogLevel = glLogLevel;
}
public override SDL_WindowFlags GetWindowFlags() => SDL_WindowFlags.SDL_WINDOW_OPENGL;
public override SDL_WindowFlags WindowFlags => SDL_WindowFlags.SDL_WINDOW_OPENGL;
protected override void InitializeWindowRenderer()
{
// Ensure to not share this context with other contexts before this point.
SetupOpenGLAttributes(false, _glLogLevel);
SetupOpenGLAttributes(false, GlLogLevel);
nint context = SDL_GL_CreateContext(WindowHandle);
CheckResult(SDL_GL_SetSwapInterval(1));

View File

@ -10,8 +10,6 @@ namespace Ryujinx.Headless
{
class VulkanWindow : WindowBase
{
private readonly GraphicsDebugLevel _glLogLevel;
public VulkanWindow(
InputManager inputManager,
GraphicsDebugLevel glLogLevel,
@ -21,10 +19,9 @@ namespace Ryujinx.Headless
bool ignoreControllerApplet)
: base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode, ignoreControllerApplet)
{
_glLogLevel = glLogLevel;
}
public override SDL_WindowFlags GetWindowFlags() => SDL_WindowFlags.SDL_WINDOW_VULKAN;
public override SDL_WindowFlags WindowFlags => SDL_WindowFlags.SDL_WINDOW_VULKAN;
protected override void InitializeWindowRenderer() { }

View File

@ -1,14 +1,15 @@
using Humanizer;
using LibHac.Tools.Fs;
using Ryujinx.Ava;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.GAL.Multithreading;
using Ryujinx.Graphics.Gpu;
using Ryujinx.Graphics.OpenGL;
using Ryujinx.HLE.HOS.Applets;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
using Ryujinx.HLE.UI;
using Ryujinx.Input;
@ -26,6 +27,7 @@ using static SDL2.SDL;
using AntiAliasing = Ryujinx.Common.Configuration.AntiAliasing;
using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter;
using Switch = Ryujinx.HLE.Switch;
using UserProfile = Ryujinx.HLE.HOS.Services.Account.Acc.UserProfile;
namespace Ryujinx.Headless
{
@ -53,8 +55,6 @@ namespace Ryujinx.Headless
public Switch Device { get; private set; }
public IRenderer Renderer { get; private set; }
public event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent;
protected nint WindowHandle { get; set; }
public IHostUITheme HostUITheme { get; }
@ -72,7 +72,7 @@ namespace Ryujinx.Headless
protected SDL2MouseDriver MouseDriver;
private readonly InputManager _inputManager;
private readonly IKeyboard _keyboardInterface;
private readonly GraphicsDebugLevel _glLogLevel;
protected readonly GraphicsDebugLevel GlLogLevel;
private readonly Stopwatch _chrono;
private readonly long _ticksPerFrame;
private readonly CancellationTokenSource _gpuCancellationTokenSource;
@ -104,7 +104,7 @@ namespace Ryujinx.Headless
NpadManager = _inputManager.CreateNpadManager();
TouchScreenManager = _inputManager.CreateTouchScreenManager();
_keyboardInterface = (IKeyboard)_inputManager.KeyboardDriver.GetGamepad("0");
_glLogLevel = glLogLevel;
GlLogLevel = glLogLevel;
_chrono = new Stopwatch();
_ticksPerFrame = Stopwatch.Frequency / TargetFps;
_gpuCancellationTokenSource = new CancellationTokenSource();
@ -137,7 +137,7 @@ namespace Ryujinx.Headless
private void SetWindowIcon()
{
Stream iconStream = typeof(Program).Assembly.GetManifestResourceStream("HeadlessLogo");
Stream iconStream = EmbeddedResources.GetStream("Ryujinx/Assets/UIImages/Logo_Ryujinx.png");
byte[] iconBytes = new byte[iconStream!.Length];
if (iconStream.Read(iconBytes, 0, iconBytes.Length) != iconBytes.Length)
@ -191,7 +191,7 @@ namespace Ryujinx.Headless
FullscreenFlag = SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP;
}
WindowHandle = SDL_CreateWindow($"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}", SDL_WINDOWPOS_CENTERED_DISPLAY(DisplayId), SDL_WINDOWPOS_CENTERED_DISPLAY(DisplayId), Width, Height, DefaultFlags | FullscreenFlag | GetWindowFlags());
WindowHandle = SDL_CreateWindow($"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}", SDL_WINDOWPOS_CENTERED_DISPLAY(DisplayId), SDL_WINDOWPOS_CENTERED_DISPLAY(DisplayId), Width, Height, DefaultFlags | FullscreenFlag | WindowFlags);
if (WindowHandle == nint.Zero)
{
@ -246,7 +246,7 @@ namespace Ryujinx.Headless
protected abstract void SwapBuffers();
public abstract SDL_WindowFlags GetWindowFlags();
public abstract SDL_WindowFlags WindowFlags { get; }
private string GetGpuDriverName()
{
@ -268,7 +268,7 @@ namespace Ryujinx.Headless
{
InitializeWindowRenderer();
Device.Gpu.Renderer.Initialize(_glLogLevel);
Device.Gpu.Renderer.Initialize(GlLogLevel);
InitializeRenderer();
@ -308,21 +308,6 @@ namespace Ryujinx.Headless
if (_ticks >= _ticksPerFrame)
{
string dockedMode = Device.System.State.DockedMode ? "Docked" : "Handheld";
float scale = GraphicsConfig.ResScale;
if (scale != 1)
{
dockedMode += $" ({scale}x)";
}
StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs(
Device.VSyncMode.ToString(),
dockedMode,
Device.Configuration.AspectRatio.ToText(),
$"{Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)",
$"FIFO: {Device.Statistics.GetFifoPercent():0.00} %",
$"GPU: {_gpuDriverName}"));
_ticks = Math.Min(_ticks - _ticksPerFrame, _ticksPerFrame);
}
}
@ -572,5 +557,10 @@ namespace Ryujinx.Headless
SDL2Driver.Instance.Dispose();
}
}
public UserProfile ShowPlayerSelectDialog()
{
return AccountSaveDataManager.GetLastUsedUser();
}
}
}

View File

@ -21,6 +21,7 @@ using Ryujinx.Graphics.Vulkan.MoltenVK;
using Ryujinx.Headless;
using Ryujinx.SDL2.Common;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
@ -46,9 +47,9 @@ namespace Ryujinx.Ava
{
Version = ReleaseInformation.Version;
if (OperatingSystem.IsWindows() && !OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17134))
if (OperatingSystem.IsWindows() && !OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041))
{
_ = MessageBoxA(nint.Zero, "You are running an outdated version of Windows.\n\nRyujinx supports Windows 10 version 1803 and newer.\n", $"Ryujinx {Version}", MbIconwarning);
_ = MessageBoxA(nint.Zero, "You are running an outdated version of Windows.\n\nRyujinx supports Windows 10 version 20H1 and newer.\n", $"Ryujinx {Version}", MbIconwarning);
}
PreviewerDetached = true;
@ -229,13 +230,16 @@ namespace Ryujinx.Ava
internal static void PrintSystemInfo()
{
Logger.Notice.Print(LogClass.Application, $"{RyujinxApp.FullAppName} Version: {Version}");
Logger.Notice.Print(LogClass.Application, $".NET Runtime: {RuntimeInformation.FrameworkDescription}");
SystemInfo.Gather().Print();
var enabledLogLevels = Logger.GetEnabledLevels().ToArray();
Logger.Notice.Print(LogClass.Application, $"Logs Enabled: {(enabledLogLevels.Length is 0
? "<None>"
: enabledLogLevels.JoinToString(", "))}");
Logger.Notice.Print(LogClass.Application, $"Logs Enabled: {
Logger.GetEnabledLevels()
.FormatCollection(
x => x.ToString(),
separator: ", ",
emptyCollectionFallback: "<None>")
}");
Logger.Notice.Print(LogClass.Application,
AppDataManager.Mode == AppDataManager.LaunchMode.Custom
@ -243,16 +247,33 @@ namespace Ryujinx.Ava
: $"Launch Mode: {AppDataManager.Mode}");
}
internal static void ProcessUnhandledException(object sender, Exception ex, bool isTerminating)
internal static void ProcessUnhandledException(object sender, Exception initialException, bool isTerminating)
{
Logger.Log log = Logger.Error ?? Logger.Notice;
string message = $"Unhandled exception caught: {ex}";
// ReSharper disable once ConstantConditionalAccessQualifier
if (sender?.GetType()?.AsPrettyString() is { } senderName)
log.Print(LogClass.Application, message, senderName);
List<Exception> exceptions = [];
if (initialException is AggregateException ae)
{
exceptions.AddRange(ae.InnerExceptions);
}
else
log.PrintMsg(LogClass.Application, message);
{
exceptions.Add(initialException);
}
foreach (var e in exceptions)
{
string message = $"Unhandled exception caught: {e}";
// ReSharper disable once ConstantConditionalAccessQualifier
if (sender?.GetType()?.AsPrettyString() is { } senderName)
log.Print(LogClass.Application, message, senderName);
else
log.PrintMsg(LogClass.Application, message);
}
if (isTerminating)
Exit();

View File

@ -123,12 +123,13 @@
<Generator>MSBuild:Compile</Generator>
</AvaloniaResource>
<AvaloniaResource Include="Assets\Styles\Styles.xaml" />
<AvaloniaResource Include="Assets\Styles\CheckboxMenuItemStyle.axaml" />
</ItemGroup>
<ItemGroup>
<None Remove="Assets\locales.json" />
<None Remove="Assets\Styles\Styles.xaml" />
<None Remove="Assets\Styles\Themes.xaml" />
<None Remove="Assets\Styles\CheckboxMenuItemStyle.xaml" />
<None Remove="Assets\Icons\Controller_JoyConLeft.svg" />
<None Remove="Assets\Icons\Controller_JoyConPair.svg" />
<None Remove="Assets\Icons\Controller_JoyConRight.svg" />
@ -150,6 +151,7 @@
</EmbeddedResource>
<EmbeddedResource Include="Assets\locales.json" />
<EmbeddedResource Include="Assets\Styles\Styles.xaml" />
<EmbeddedResource Include="Assets\Styles\CheckboxMenuItemStyle.axaml" />
<EmbeddedResource Include="Assets\Icons\Controller_JoyConLeft.svg" />
<EmbeddedResource Include="Assets\Icons\Controller_JoyConPair.svg" />
<EmbeddedResource Include="Assets\Icons\Controller_JoyConRight.svg" />
@ -166,7 +168,6 @@
<EmbeddedResource Include="Assets\UIImages\Logo_GitHub_Light.png" />
<EmbeddedResource Include="Assets\UIImages\Logo_Ryujinx.png" />
<EmbeddedResource Include="Assets\UIImages\Logo_Ryujinx_AntiAlias.png" />
<EmbeddedResource Include="Headless\Ryujinx.bmp" LogicalName="HeadlessLogo" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="Assets\locales.json" />
@ -174,4 +175,5 @@
<ItemGroup>
<Folder Include="Assets\Fonts\Mono\" />
</ItemGroup>
</Project>

View File

@ -16,5 +16,6 @@
<Application.Styles>
<sty:FluentAvaloniaTheme PreferUserAccentColor="True" PreferSystemTheme="False" />
<StyleInclude Source="/Assets/Styles/Styles.xaml" />
<StyleInclude Source="/Assets/Styles/CheckboxMenuItemStyle.axaml"/>
</Application.Styles>
</Application>

View File

@ -1,17 +1,23 @@
using Avalonia.Controls;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
using Gommon;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Ava.Utilities.Configuration;
using Ryujinx.Common;
using Ryujinx.HLE;
using Ryujinx.HLE.HOS.Applets;
using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
using Ryujinx.HLE.UI;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
namespace Ryujinx.Ava.UI.Applet
@ -35,7 +41,7 @@ namespace Ryujinx.Ava.UI.Applet
bool okPressed = false;
if (ConfigurationState.Instance.IgnoreApplet)
if (ConfigurationState.Instance.System.IgnoreApplet)
return false;
Dispatcher.UIThread.InvokeAsync(async () =>
@ -253,5 +259,59 @@ namespace Ryujinx.Ava.UI.Applet
}
public IDynamicTextInputHandler CreateDynamicTextInputHandler() => new AvaloniaDynamicTextInputHandler(_parent);
public UserProfile ShowPlayerSelectDialog()
{
UserId selected = UserId.Null;
byte[] defaultGuestImage = EmbeddedResources.Read("Ryujinx.HLE/HOS/Services/Account/Acc/GuestUserImage.jpg");
UserProfile guest = new UserProfile(new UserId("00000000000000000000000000000080"), "Guest", defaultGuestImage);
ManualResetEvent dialogCloseEvent = new(false);
Dispatcher.UIThread.InvokeAsync(async () =>
{
ObservableCollection<BaseModel> profiles = [];
NavigationDialogHost nav = new();
_parent.AccountManager.GetAllUsers()
.OrderBy(x => x.Name)
.ForEach(profile => profiles.Add(new Models.UserProfile(profile, nav)));
profiles.Add(new Models.UserProfile(guest, nav));
UserSelectorDialogViewModel viewModel = new()
{
Profiles = profiles,
SelectedUserId = _parent.AccountManager.LastOpenedUser.UserId
};
UserSelectorDialog content = new(viewModel);
(selected, _) = await UserSelectorDialog.ShowInputDialog(content);
dialogCloseEvent.Set();
});
dialogCloseEvent.WaitOne();
UserProfile profile = _parent.AccountManager.LastOpenedUser;
if (selected == guest.UserId)
{
profile = guest;
}
else if (selected == UserId.Null)
{
profile = null;
}
else
{
foreach (UserProfile p in _parent.AccountManager.GetAllUsers())
{
if (p.UserId == selected)
{
profile = p;
break;
}
}
}
return profile;
}
}
}

View File

@ -0,0 +1,121 @@
<UserControl
x:Class="Ryujinx.Ava.UI.Applet.UserSelectorDialog"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
d:DesignHeight="450"
MinWidth="500"
d:DesignWidth="800"
mc:Ignorable="d"
Focusable="True"
x:DataType="viewModels:UserSelectorDialogViewModel">
<UserControl.Resources>
<helpers:BitmapArrayValueConverter x:Key="ByteImage" />
</UserControl.Resources>
<Design.DataContext>
<viewModels:UserSelectorDialogViewModel />
</Design.DataContext>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border
CornerRadius="5"
BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
BorderThickness="1">
<ListBox
MaxHeight="300"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Background="Transparent"
ItemsSource="{Binding Profiles}"
SelectionChanged="ProfilesList_SelectionChanged">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel
HorizontalAlignment="Left"
VerticalAlignment="Center"
Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Margin" Value="5 5 0 5" />
<Setter Property="CornerRadius" Value="5" />
</Style>
<Style Selector="Rectangle#SelectionIndicator">
<Setter Property="Opacity" Value="0" />
</Style>
</ListBox.Styles>
<ListBox.DataTemplates>
<DataTemplate
DataType="models:UserProfile">
<Grid
PointerEntered="Grid_PointerEntered"
PointerExited="Grid_OnPointerExited">
<Border
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ClipToBounds="True"
CornerRadius="5"
Background="{Binding BackgroundColor}">
<StackPanel
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Image
Width="96"
Height="96"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Source="{Binding Image, Converter={StaticResource ByteImage}}" />
<TextBlock
HorizontalAlignment="Stretch"
MaxWidth="90"
Text="{Binding Name}"
TextAlignment="Center"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis"
MaxLines="2"
Margin="5" />
</StackPanel>
</Border>
</Grid>
</DataTemplate>
<DataTemplate
DataType="viewModels:BaseModel">
<Panel
Height="118"
Width="96">
<Panel.Styles>
<Style Selector="Panel">
<Setter Property="Background" Value="{DynamicResource ListBoxBackground}" />
</Style>
</Panel.Styles>
</Panel>
</DataTemplate>
</ListBox.DataTemplates>
</ListBox>
</Border>
<StackPanel
Grid.Row="1"
Margin="0 24 0 0"
HorizontalAlignment="Left"
Orientation="Horizontal"
Spacing="10">
</StackPanel>
</Grid>
</UserControl>

View File

@ -0,0 +1,123 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.ViewModels.Input;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
using UserProfileSft = Ryujinx.HLE.HOS.Services.Account.Acc.UserProfile;
namespace Ryujinx.Ava.UI.Applet
{
public partial class UserSelectorDialog : UserControl, INotifyPropertyChanged
{
public UserSelectorDialogViewModel ViewModel { get; set; }
public UserSelectorDialog(UserSelectorDialogViewModel viewModel)
{
InitializeComponent();
ViewModel = viewModel;
DataContext = ViewModel;
}
private void Grid_PointerEntered(object sender, PointerEventArgs e)
{
if (sender is Grid { DataContext: UserProfile profile })
{
profile.IsPointerOver = true;
}
}
private void Grid_OnPointerExited(object sender, PointerEventArgs e)
{
if (sender is Grid { DataContext: UserProfile profile })
{
profile.IsPointerOver = false;
}
}
private void ProfilesList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is ListBox listBox)
{
int selectedIndex = listBox.SelectedIndex;
if (selectedIndex >= 0 && selectedIndex < ViewModel.Profiles.Count)
{
if (ViewModel.Profiles[selectedIndex] is UserProfile userProfile)
{
ViewModel.SelectedUserId = userProfile.UserId;
Logger.Info?.Print(LogClass.UI, $"Selected user: {userProfile.UserId}");
ObservableCollection<BaseModel> newProfiles = [];
foreach (var item in ViewModel.Profiles)
{
if (item is UserProfile originalItem)
{
var profile = new UserProfileSft(originalItem.UserId, originalItem.Name, originalItem.Image);
if (profile.UserId == ViewModel.SelectedUserId)
{
profile.AccountState = AccountState.Open;
}
newProfiles.Add(new UserProfile(profile, new NavigationDialogHost()));
}
}
ViewModel.Profiles = newProfiles;
}
}
}
}
public static async Task<(UserId Id, bool Result)> ShowInputDialog(UserSelectorDialog content)
{
ContentDialog contentDialog = new()
{
Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle],
PrimaryButtonText = LocaleManager.Instance[LocaleKeys.Continue],
SecondaryButtonText = string.Empty,
CloseButtonText = LocaleManager.Instance[LocaleKeys.Cancel],
Content = content,
Padding = new Thickness(0)
};
UserId result = UserId.Null;
bool input = false;
void Handler(ContentDialog sender, ContentDialogClosedEventArgs eventArgs)
{
if (eventArgs.Result == ContentDialogResult.Primary)
{
if (contentDialog.Content is UserSelectorDialog view)
{
result = view.ViewModel.SelectedUserId;
input = true;
}
}
else
{
result = UserId.Null;
input = false;
}
}
contentDialog.Closed += Handler;
await ContentDialogHelper.ShowAsync(contentDialog);
return (result, input);
}
}
}

View File

@ -106,6 +106,10 @@
Click="ExtractApplicationRomFs_Click"
Header="{ext:Locale GameListContextMenuExtractDataRomFS}"
ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataRomFSToolTip}" />
<MenuItem
Click="ExtractAocRomFs_Click"
Header="{ext:Locale GameListContextMenuExtractDataAocRomFS}"
ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataAocRomFSToolTip}" />
<MenuItem
Click="ExtractApplicationLogo_Click"
Header="{ext:Locale GameListContextMenuExtractDataLogo}"

View File

@ -3,10 +3,13 @@ using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
using LibHac;
using LibHac.Fs;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Common;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Common.Models;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows;
@ -14,6 +17,7 @@ using Ryujinx.Ava.Utilities;
using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Helper;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS;
using SkiaSharp;
using System;
@ -247,7 +251,7 @@ namespace Ryujinx.Ava.UI.Controls
{
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
{
string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "shader");
string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString.ToLower(), "cache", "shader");
if (!Directory.Exists(shaderCacheDir))
{
@ -279,6 +283,22 @@ namespace Ryujinx.Ava.UI.Controls
viewModel.SelectedApplication.Path,
viewModel.SelectedApplication.Name);
}
public async void ExtractAocRomFs_Click(object sender, RoutedEventArgs args)
{
if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
return;
DownloadableContentModel selectedDlc = await DlcSelectView.Show(viewModel.SelectedApplication.IdBase, viewModel.ApplicationLibrary);
if (selectedDlc is not null)
{
await ApplicationHelper.ExtractAoc(
viewModel.StorageProvider,
selectedDlc.ContainerPath,
selectedDlc.FileName);
}
}
public async void ExtractApplicationLogo_Click(object sender, RoutedEventArgs args)
{

View File

@ -2,11 +2,10 @@
x:Class="Ryujinx.Ava.UI.Controls.ApplicationListView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="450"
d:DesignWidth="800"
Focusable="True"
@ -133,12 +132,13 @@
Spacing="5">
<TextBlock
HorizontalAlignment="Stretch"
Text="{Binding TimePlayedString}"
Text="{Binding LastPlayedString}"
TextAlignment="End"
TextWrapping="Wrap" />
<TextBlock
HorizontalAlignment="Stretch"
Text="{Binding LastPlayedString}"
Text="{Binding TimePlayedString}"
IsVisible="{Binding HasPlayedPreviously}"
TextAlignment="End"
TextWrapping="Wrap" />
<TextBlock

View File

@ -0,0 +1,67 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:helpers="using:Ryujinx.Ava.UI.Helpers"
xmlns:ext="using:Ryujinx.Ava.Common.Markup"
xmlns:models="using:Ryujinx.Ava.Common.Models"
xmlns:viewModels="using:Ryujinx.Ava.UI.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ryujinx.Ava.UI.Controls.DlcSelectView"
x:DataType="viewModels:DlcSelectViewModel">
<Grid RowDefinitions="*,Auto,*">
<TextBlock
Classes="h1"
Margin="5, -5, 0, 15"
HorizontalAlignment="Center"
VerticalAlignment="Center"
TextWrapping="Wrap"
Text="{ext:Locale ExtractAocListHeader}" />
<ScrollViewer Grid.Row="2">
<ListBox
AutoScrollToSelectedItem="False"
SelectionMode="Single"
Background="Transparent"
SelectedItem="{Binding SelectedDlc}"
ItemsSource="{Binding Dlcs}">
<ListBox.DataTemplates>
<DataTemplate
DataType="models:DownloadableContentModel">
<Panel Margin="10" Background="Transparent">
<Grid ColumnDefinitions="*,Auto">
<Grid
Grid.Column="0" ColumnDefinitions="*,Auto">
<TextBlock
Grid.Column="0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
MaxLines="2"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis">
<TextBlock.Text>
<MultiBinding Converter="{x:Static helpers:DownloadableContentLabelConverter.Instance}">
<Binding Path="FileName" />
<Binding Path="IsBundled" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<TextBlock
Grid.Column="1"
Margin="10, 0, 0, 0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding TitleIdStr}" />
</Grid>
</Grid>
</Panel>
</DataTemplate>
</ListBox.DataTemplates>
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Background" Value="Transparent" />
</Style>
</ListBox.Styles>
</ListBox>
</ScrollViewer>
</Grid>
</UserControl>

View File

@ -0,0 +1,51 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Styling;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Common.Models;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.Utilities.AppLibrary;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.Controls
{
public partial class DlcSelectView : UserControl
{
public DlcSelectView()
{
InitializeComponent();
}
#nullable enable
public static async Task<DownloadableContentModel?> Show(ulong selectedTitleId, ApplicationLibrary appLibrary)
#nullable disable
{
DlcSelectViewModel viewModel = new(selectedTitleId, appLibrary);
ContentDialog contentDialog = new()
{
PrimaryButtonText = LocaleManager.Instance[LocaleKeys.Continue],
SecondaryButtonText = string.Empty,
CloseButtonText = string.Empty,
Content = new DlcSelectView { DataContext = viewModel }
};
Style closeButton = new(x => x.Name("CloseButton"));
closeButton.Setters.Add(new Setter(WidthProperty, 80d));
Style closeButtonParent = new(x => x.Name("CommandSpace"));
closeButtonParent.Setters.Add(new Setter(HorizontalAlignmentProperty,
Avalonia.Layout.HorizontalAlignment.Right));
contentDialog.Styles.Add(closeButton);
contentDialog.Styles.Add(closeButtonParent);
await ContentDialogHelper.ShowAsync(contentDialog);
return viewModel.SelectedDlc;
}
}
}

View File

@ -69,7 +69,7 @@ namespace Ryujinx.Ava.UI.Controls
VirtualFileSystem ownerVirtualFileSystem,
HorizonClient ownerHorizonClient)
{
var content = new NavigationDialogHost(ownerAccountManager, ownerContentManager, ownerVirtualFileSystem, ownerHorizonClient);
NavigationDialogHost content = new(ownerAccountManager, ownerContentManager, ownerVirtualFileSystem, ownerHorizonClient);
ContentDialog contentDialog = new()
{
Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle],

View File

@ -7,6 +7,7 @@
Title="Ryujinx - Waiting"
SizeToContent="WidthAndHeight"
WindowStartupLocation="CenterOwner"
CanResize="False"
mc:Ignorable="d"
Focusable="True">
<Grid

View File

@ -0,0 +1,48 @@
using CommunityToolkit.Mvvm.Input;
using System;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.Helpers
{
#nullable enable
public static class Commands
{
public static RelayCommand Create(Action action)
=> new(action);
public static RelayCommand CreateConditional(Action action, Func<bool> canExecute)
=> new(action, canExecute);
public static RelayCommand<T> Create<T>(Action<T?> action)
=> new(action);
public static RelayCommand<T> CreateConditional<T>(Action<T?> action, Predicate<T?> canExecute)
=> new(action, canExecute);
public static AsyncRelayCommand Create(Func<Task> action)
=> new(action, AsyncRelayCommandOptions.None);
public static AsyncRelayCommand CreateConcurrent(Func<Task> action)
=> new(action, AsyncRelayCommandOptions.AllowConcurrentExecutions);
public static AsyncRelayCommand CreateSilentFail(Func<Task> action)
=> new(action, AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler);
public static AsyncRelayCommand<T> Create<T>(Func<T?, Task> action)
=> new(action, AsyncRelayCommandOptions.None);
public static AsyncRelayCommand<T> CreateConcurrent<T>(Func<T?, Task> action)
=> new(action, AsyncRelayCommandOptions.AllowConcurrentExecutions);
public static AsyncRelayCommand<T> CreateSilentFail<T>(Func<T?, Task> action)
=> new(action, AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler);
public static AsyncRelayCommand CreateConditional(Func<Task> action, Func<bool> canExecute)
=> new(action, canExecute, AsyncRelayCommandOptions.None);
public static AsyncRelayCommand CreateConcurrentConditional(Func<Task> action, Func<bool> canExecute)
=> new(action, canExecute, AsyncRelayCommandOptions.AllowConcurrentExecutions);
public static AsyncRelayCommand CreateSilentFailConditional(Func<Task> action, Func<bool> canExecute)
=> new(action, canExecute, AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler);
public static AsyncRelayCommand<T> CreateConditional<T>(Func<T?, Task> action, Predicate<T?> canExecute)
=> new(action, canExecute, AsyncRelayCommandOptions.None);
public static AsyncRelayCommand<T> CreateConcurrentConditional<T>(Func<T?, Task> action, Predicate<T?> canExecute)
=> new(action, canExecute, AsyncRelayCommandOptions.AllowConcurrentExecutions);
public static AsyncRelayCommand<T> CreateSilentFailConditional<T>(Func<T?, Task> action, Predicate<T?> canExecute)
=> new(action, canExecute, AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler);
}
}

View File

@ -1,3 +1,4 @@
using Avalonia.Media;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
@ -387,6 +388,30 @@ namespace Ryujinx.Ava.UI.Models.Input
}
}
private bool _enableLedChanging;
public bool EnableLedChanging
{
get => _enableLedChanging;
set
{
_enableLedChanging = value;
OnPropertyChanged();
}
}
private Color _ledColor;
public Color LedColor
{
get => _ledColor;
set
{
_ledColor = value;
OnPropertyChanged();
}
}
private bool _enableMotion;
public bool EnableMotion
{
@ -483,12 +508,23 @@ namespace Ryujinx.Ava.UI.Models.Input
WeakRumble = controllerInput.Rumble.WeakRumble;
StrongRumble = controllerInput.Rumble.StrongRumble;
}
if (controllerInput.Led != null)
{
EnableLedChanging = controllerInput.Led.EnableLed;
uint rawColor = controllerInput.Led.LedColor;
byte alpha = (byte)(rawColor >> 24);
byte red = (byte)(rawColor >> 16);
byte green = (byte)(rawColor >> 8);
byte blue = (byte)(rawColor % 256);
LedColor = new Color(alpha, red, green, blue);
}
}
}
public InputConfig GetConfig()
{
var config = new StandardControllerInputConfig
StandardControllerInputConfig config = new()
{
Id = Id,
Backend = InputBackendType.GamepadSDL2,
@ -540,6 +576,11 @@ namespace Ryujinx.Ava.UI.Models.Input
WeakRumble = WeakRumble,
StrongRumble = StrongRumble,
},
Led = new LedConfigController
{
EnableLed = EnableLedChanging,
LedColor = LedColor.ToUInt32()
},
Version = InputConfig.CurrentVersion,
DeadzoneLeft = DeadzoneLeft,
DeadzoneRight = DeadzoneRight,

View File

@ -1,152 +1,53 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Common.Configuration.Hid;
namespace Ryujinx.Ava.UI.Models.Input
{
public class HotkeyConfig : BaseModel
public partial class HotkeyConfig : BaseModel
{
private Key _toggleVSyncMode;
public Key ToggleVSyncMode
{
get => _toggleVSyncMode;
set
{
_toggleVSyncMode = value;
OnPropertyChanged();
}
}
[ObservableProperty] private Key _toggleVSyncMode;
private Key _screenshot;
public Key Screenshot
{
get => _screenshot;
set
{
_screenshot = value;
OnPropertyChanged();
}
}
[ObservableProperty] private Key _screenshot;
private Key _showUI;
public Key ShowUI
{
get => _showUI;
set
{
_showUI = value;
OnPropertyChanged();
}
}
[ObservableProperty] private Key _showUI;
private Key _pause;
public Key Pause
{
get => _pause;
set
{
_pause = value;
OnPropertyChanged();
}
}
[ObservableProperty] private Key _pause;
private Key _toggleMute;
public Key ToggleMute
{
get => _toggleMute;
set
{
_toggleMute = value;
OnPropertyChanged();
}
}
[ObservableProperty] private Key _toggleMute;
private Key _resScaleUp;
public Key ResScaleUp
{
get => _resScaleUp;
set
{
_resScaleUp = value;
OnPropertyChanged();
}
}
[ObservableProperty] private Key _resScaleUp;
private Key _resScaleDown;
public Key ResScaleDown
{
get => _resScaleDown;
set
{
_resScaleDown = value;
OnPropertyChanged();
}
}
[ObservableProperty] private Key _resScaleDown;
private Key _volumeUp;
public Key VolumeUp
{
get => _volumeUp;
set
{
_volumeUp = value;
OnPropertyChanged();
}
}
[ObservableProperty] private Key _volumeUp;
private Key _volumeDown;
public Key VolumeDown
{
get => _volumeDown;
set
{
_volumeDown = value;
OnPropertyChanged();
}
}
[ObservableProperty] private Key _volumeDown;
private Key _customVSyncIntervalIncrement;
public Key CustomVSyncIntervalIncrement
{
get => _customVSyncIntervalIncrement;
set
{
_customVSyncIntervalIncrement = value;
OnPropertyChanged();
}
}
[ObservableProperty] private Key _customVSyncIntervalIncrement;
private Key _customVSyncIntervalDecrement;
public Key CustomVSyncIntervalDecrement
{
get => _customVSyncIntervalDecrement;
set
{
_customVSyncIntervalDecrement = value;
OnPropertyChanged();
}
}
[ObservableProperty] private Key _customVSyncIntervalDecrement;
public HotkeyConfig(KeyboardHotkeys config)
{
if (config != null)
{
ToggleVSyncMode = config.ToggleVSyncMode;
Screenshot = config.Screenshot;
ShowUI = config.ShowUI;
Pause = config.Pause;
ToggleMute = config.ToggleMute;
ResScaleUp = config.ResScaleUp;
ResScaleDown = config.ResScaleDown;
VolumeUp = config.VolumeUp;
VolumeDown = config.VolumeDown;
CustomVSyncIntervalIncrement = config.CustomVSyncIntervalIncrement;
CustomVSyncIntervalDecrement = config.CustomVSyncIntervalDecrement;
}
if (config == null)
return;
ToggleVSyncMode = config.ToggleVSyncMode;
Screenshot = config.Screenshot;
ShowUI = config.ShowUI;
Pause = config.Pause;
ToggleMute = config.ToggleMute;
ResScaleUp = config.ResScaleUp;
ResScaleDown = config.ResScaleDown;
VolumeUp = config.VolumeUp;
VolumeDown = config.VolumeDown;
CustomVSyncIntervalIncrement = config.CustomVSyncIntervalIncrement;
CustomVSyncIntervalDecrement = config.CustomVSyncIntervalDecrement;
}
public KeyboardHotkeys GetConfig()
{
var config = new KeyboardHotkeys
public KeyboardHotkeys GetConfig() =>
new()
{
ToggleVSyncMode = ToggleVSyncMode,
Screenshot = Screenshot,
@ -160,8 +61,5 @@ namespace Ryujinx.Ava.UI.Models.Input
CustomVSyncIntervalIncrement = CustomVSyncIntervalIncrement,
CustomVSyncIntervalDecrement = CustomVSyncIntervalDecrement,
};
return config;
}
}
}

View File

@ -22,5 +22,22 @@ namespace Ryujinx.Ava.UI.Models
FifoStatus = fifoStatus;
ShaderCount = shaderCount;
}
public override bool Equals(object obj)
{
if (obj is not StatusUpdatedEventArgs suea) return false;
return
VSyncMode == suea.VSyncMode &&
VolumeStatus == suea.VolumeStatus &&
DockedMode == suea.DockedMode &&
AspectRatio == suea.AspectRatio &&
GameStatus == suea.GameStatus &&
FifoStatus == suea.FifoStatus &&
ShaderCount == suea.ShaderCount;
}
public override int GetHashCode()
=> HashCode.Combine(VSyncMode, VolumeStatus, AspectRatio, DockedMode, FifoStatus, GameStatus, ShaderCount);
}
}

View File

@ -27,7 +27,7 @@ namespace Ryujinx.Ava.UI.ViewModels
ThemeManager.ThemeChanged += ThemeManager_ThemeChanged;
}
private void ThemeManager_ThemeChanged(object sender, EventArgs e)
private void ThemeManager_ThemeChanged()
{
Dispatcher.UIThread.Post(() => UpdateLogoTheme(ConfigurationState.Instance.UI.BaseStyle.Value));
}

View File

@ -432,7 +432,7 @@ namespace Ryujinx.Ava.UI.ViewModels
{
try
{
HttpResponseMessage response = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/Amiibo.json"));
HttpResponseMessage response = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, "https://raw.githubusercontent.com/Ryubing/Ryujinx/refs/heads/master/assets/amiibo/Amiibo.json"));
if (response.IsSuccessStatusCode)
{
@ -451,7 +451,7 @@ namespace Ryujinx.Ava.UI.ViewModels
{
try
{
HttpResponseMessage response = await _httpClient.GetAsync($"https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/Amiibo.json");
HttpResponseMessage response = await _httpClient.GetAsync($"https://raw.githubusercontent.com/Ryubing/Ryujinx/refs/heads/master/assets/amiibo/Amiibo.json");
if (response.IsSuccessStatusCode)
{

View File

@ -0,0 +1,25 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Ryujinx.Ava.Common.Models;
using Ryujinx.Ava.Utilities.AppLibrary;
using System.Linq;
namespace Ryujinx.Ava.UI.ViewModels
{
public partial class DlcSelectViewModel : BaseModel
{
[ObservableProperty] private DownloadableContentModel[] _dlcs;
#nullable enable
[ObservableProperty] private DownloadableContentModel? _selectedDlc;
#nullable disable
public DlcSelectViewModel(ulong titleId, ApplicationLibrary appLibrary)
{
_dlcs = appLibrary.DownloadableContents.Items
.Where(x => x.Dlc.TitleIdBase == titleId)
.Select(x => x.Dlc)
.OrderBy(it => it.IsBundled ? 0 : 1)
.ThenBy(it => it.TitleId)
.ToArray();
}
}
}

View File

@ -1,21 +1,13 @@
using Avalonia.Svg.Skia;
using CommunityToolkit.Mvvm.ComponentModel;
using Ryujinx.Ava.UI.Models.Input;
using Ryujinx.Ava.UI.Views.Input;
namespace Ryujinx.Ava.UI.ViewModels.Input
{
public class ControllerInputViewModel : BaseModel
public partial class ControllerInputViewModel : BaseModel
{
private GamepadInputConfig _config;
public GamepadInputConfig Config
{
get => _config;
set
{
_config = value;
OnPropertyChanged();
}
}
[ObservableProperty] private GamepadInputConfig _config;
private bool _isLeft;
public bool IsLeft
@ -43,18 +35,9 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public bool HasSides => IsLeft ^ IsRight;
private SvgImage _image;
public SvgImage Image
{
get => _image;
set
{
_image = value;
OnPropertyChanged();
}
}
[ObservableProperty] private SvgImage _image;
public readonly InputViewModel ParentModel;
public InputViewModel ParentModel { get; }
public ControllerInputViewModel(InputViewModel model, GamepadInputConfig config)
{

View File

@ -1,9 +1,8 @@
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Svg.Skia;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Input;
using Ryujinx.Ava.UI.Helpers;
@ -32,7 +31,7 @@ using Key = Ryujinx.Common.Configuration.Hid.Key;
namespace Ryujinx.Ava.UI.ViewModels.Input
{
public class InputViewModel : BaseModel, IDisposable
public partial class InputViewModel : BaseModel, IDisposable
{
private const string Disabled = "disabled";
private const string ProControllerResource = "Ryujinx/Assets/Icons/Controller_ProCon.svg";
@ -48,8 +47,8 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
private int _controller;
private string _controllerImage;
private int _device;
private object _configViewModel;
private string _profileName;
[ObservableProperty] private object _configViewModel;
[ObservableProperty] private string _profileName;
private bool _isLoaded;
private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
@ -70,20 +69,12 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public bool IsRight { get; set; }
public bool IsLeft { get; set; }
public bool HasLed => false; //temporary
//SelectedGamepad.Features.HasFlag(GamepadFeaturesFlag.Led);
public bool IsModified { get; set; }
public event Action NotifyChangesEvent;
public object ConfigViewModel
{
get => _configViewModel;
set
{
_configViewModel = value;
OnPropertyChanged();
}
}
public PlayerIndex PlayerIdChoose
{
get => _playerIdChoose;
@ -200,16 +191,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
}
}
public string ProfileName
{
get => _profileName; set
{
_profileName = value;
OnPropertyChanged();
}
}
public int Device
{
get => _device;

View File

@ -1,20 +1,12 @@
using Avalonia.Svg.Skia;
using CommunityToolkit.Mvvm.ComponentModel;
using Ryujinx.Ava.UI.Models.Input;
namespace Ryujinx.Ava.UI.ViewModels.Input
{
public class KeyboardInputViewModel : BaseModel
public partial class KeyboardInputViewModel : BaseModel
{
private KeyboardInputConfig _config;
public KeyboardInputConfig Config
{
get => _config;
set
{
_config = value;
OnPropertyChanged();
}
}
[ObservableProperty] private KeyboardInputConfig _config;
private bool _isLeft;
public bool IsLeft
@ -42,16 +34,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public bool HasSides => IsLeft ^ IsRight;
private SvgImage _image;
public SvgImage Image
{
get => _image;
set
{
_image = value;
OnPropertyChanged();
}
}
[ObservableProperty] private SvgImage _image;
public readonly InputViewModel ParentModel;

View File

@ -1,93 +1,23 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Ryujinx.Ava.UI.ViewModels.Input
{
public class MotionInputViewModel : BaseModel
public partial class MotionInputViewModel : BaseModel
{
private int _slot;
public int Slot
{
get => _slot;
set
{
_slot = value;
OnPropertyChanged();
}
}
[ObservableProperty] private int _slot;
private int _altSlot;
public int AltSlot
{
get => _altSlot;
set
{
_altSlot = value;
OnPropertyChanged();
}
}
[ObservableProperty] private int _altSlot;
private string _dsuServerHost;
public string DsuServerHost
{
get => _dsuServerHost;
set
{
_dsuServerHost = value;
OnPropertyChanged();
}
}
[ObservableProperty] private string _dsuServerHost;
private int _dsuServerPort;
public int DsuServerPort
{
get => _dsuServerPort;
set
{
_dsuServerPort = value;
OnPropertyChanged();
}
}
[ObservableProperty] private int _dsuServerPort;
private bool _mirrorInput;
public bool MirrorInput
{
get => _mirrorInput;
set
{
_mirrorInput = value;
OnPropertyChanged();
}
}
[ObservableProperty] private bool _mirrorInput;
private int _sensitivity;
public int Sensitivity
{
get => _sensitivity;
set
{
_sensitivity = value;
OnPropertyChanged();
}
}
[ObservableProperty] private int _sensitivity;
private double _gryoDeadzone;
public double GyroDeadzone
{
get => _gryoDeadzone;
set
{
_gryoDeadzone = value;
OnPropertyChanged();
}
}
[ObservableProperty] private double _gyroDeadzone;
private bool _enableCemuHookMotion;
public bool EnableCemuHookMotion
{
get => _enableCemuHookMotion;
set
{
_enableCemuHookMotion = value;
OnPropertyChanged();
}
}
[ObservableProperty] private bool _enableCemuHookMotion;
}
}

View File

@ -1,27 +1,11 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Ryujinx.Ava.UI.ViewModels.Input
{
public class RumbleInputViewModel : BaseModel
public partial class RumbleInputViewModel : BaseModel
{
private float _strongRumble;
public float StrongRumble
{
get => _strongRumble;
set
{
_strongRumble = value;
OnPropertyChanged();
}
}
[ObservableProperty] private float _strongRumble;
private float _weakRumble;
public float WeakRumble
{
get => _weakRumble;
set
{
_weakRumble = value;
OnPropertyChanged();
}
}
[ObservableProperty] private float _weakRumble;
}
}

View File

@ -38,7 +38,6 @@ using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption;
using Ryujinx.HLE.UI;
using Ryujinx.Input.HLE;
using Silk.NET.Vulkan;
using SkiaSharp;
using System;
using System.Collections.Generic;
@ -489,6 +488,19 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
public bool StartGamesWithoutUI
{
get => ConfigurationState.Instance.UI.StartNoUI;
set
{
ConfigurationState.Instance.UI.StartNoUI.Value = value;
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
OnPropertyChanged();
}
}
public bool ShowConsole
{
get => ConfigurationState.Instance.UI.ShowConsole;
@ -625,6 +637,7 @@ namespace Ryujinx.Ava.UI.ViewModels
ApplicationSort.FileSize => LocaleManager.Instance[LocaleKeys.GameListHeaderFileSize],
ApplicationSort.Path => LocaleManager.Instance[LocaleKeys.GameListHeaderPath],
ApplicationSort.Favorite => LocaleManager.Instance[LocaleKeys.CommonFavorite],
ApplicationSort.TitleId => LocaleManager.Instance[LocaleKeys.DlcManagerTableHeadingTitleIdLabel],
_ => string.Empty,
};
}
@ -695,6 +708,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public IHostUIHandler UiHandler { get; internal set; }
public bool IsSortedByFavorite => SortMode == ApplicationSort.Favorite;
public bool IsSortedByTitle => SortMode == ApplicationSort.Title;
public bool IsSortedByTitleId => SortMode == ApplicationSort.TitleId;
public bool IsSortedByDeveloper => SortMode == ApplicationSort.Developer;
public bool IsSortedByLastPlayed => SortMode == ApplicationSort.LastPlayed;
public bool IsSortedByTimePlayed => SortMode == ApplicationSort.TotalTimePlayed;
@ -727,6 +741,7 @@ namespace Ryujinx.Ava.UI.ViewModels
ApplicationSort.FileSize => CreateComparer(IsAscending, app => app.FileSize),
ApplicationSort.Path => CreateComparer(IsAscending, app => app.Path),
ApplicationSort.Favorite => CreateComparer(IsAscending, app => new AppListFavoriteComparable(app)),
ApplicationSort.TitleId => CreateComparer(IsAscending, app => app.Id),
_ => null,
#pragma warning restore IDE0055
};
@ -743,7 +758,7 @@ namespace Ryujinx.Ava.UI.ViewModels
.Sort(GetComparer())
#pragma warning disable MVVMTK0034
.Bind(out _appsObservableList)
#pragma warning enable MVVMTK0034
#pragma warning restore MVVMTK0034
.AsObservableList();
OnPropertyChanged(nameof(AppsObservableList));
@ -1196,6 +1211,11 @@ namespace Ryujinx.Ava.UI.ViewModels
StartGamesInFullscreen = !StartGamesInFullscreen;
}
public void ToggleStartGamesWithoutUI()
{
StartGamesWithoutUI = !StartGamesWithoutUI;
}
public void ToggleShowConsole()
{
ShowConsole = !ShowConsole;
@ -1642,10 +1662,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public async Task OpenAmiiboWindow()
{
if (!IsAmiiboRequested)
return;
if (AppHost.Device.System.SearchingForAmiibo(out int deviceId))
if (AppHost.Device.System.SearchingForAmiibo(out int deviceId) && IsGameRunning)
{
string titleId = AppHost.Device.Processes.ActiveApplication.ProgramIdText.ToUpper();
AmiiboWindow window = new(ShowAll, LastScannedAmiiboId, titleId);
@ -1663,10 +1680,7 @@ namespace Ryujinx.Ava.UI.ViewModels
}
public async Task OpenBinFile()
{
if (!IsAmiiboRequested)
return;
if (AppHost.Device.System.SearchingForAmiibo(out int deviceId))
if (AppHost.Device.System.SearchingForAmiibo(out _) && IsGameRunning)
{
var result = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{

View File

@ -488,7 +488,6 @@ namespace Ryujinx.Ava.UI.ViewModels
EnableDiscordIntegration = config.EnableDiscordIntegration;
CheckUpdatesOnStart = config.CheckUpdatesOnStart;
ShowConfirmExit = config.ShowConfirmExit;
IgnoreApplet = config.IgnoreApplet;
RememberWindowState = config.RememberWindowState;
ShowTitleBar = config.ShowTitleBar;
HideCursor = (int)config.HideCursor.Value;
@ -532,6 +531,7 @@ namespace Ryujinx.Ava.UI.ViewModels
EnableFsIntegrityChecks = config.System.EnableFsIntegrityChecks;
DramSize = config.System.DramSize;
IgnoreMissingServices = config.System.IgnoreMissingServices;
IgnoreApplet = config.System.IgnoreApplet;
// CPU
EnablePptc = config.System.EnablePtc;
@ -591,7 +591,6 @@ namespace Ryujinx.Ava.UI.ViewModels
config.EnableDiscordIntegration.Value = EnableDiscordIntegration;
config.CheckUpdatesOnStart.Value = CheckUpdatesOnStart;
config.ShowConfirmExit.Value = ShowConfirmExit;
config.IgnoreApplet.Value = IgnoreApplet;
config.RememberWindowState.Value = RememberWindowState;
config.ShowTitleBar.Value = ShowTitleBar;
config.HideCursor.Value = (HideCursorMode)HideCursor;
@ -632,12 +631,10 @@ namespace Ryujinx.Ava.UI.ViewModels
}
config.System.SystemTimeOffset.Value = Convert.ToInt64((CurrentDate.ToUnixTimeSeconds() + CurrentTime.TotalSeconds) - DateTimeOffset.Now.ToUnixTimeSeconds());
config.Graphics.VSyncMode.Value = VSyncMode;
config.Graphics.EnableCustomVSyncInterval.Value = EnableCustomVSyncInterval;
config.Graphics.CustomVSyncInterval.Value = CustomVSyncInterval;
config.System.EnableFsIntegrityChecks.Value = EnableFsIntegrityChecks;
config.System.DramSize.Value = DramSize;
config.System.IgnoreMissingServices.Value = IgnoreMissingServices;
config.System.IgnoreApplet.Value = IgnoreApplet;
// CPU
config.System.EnablePtc.Value = EnablePptc;
@ -646,6 +643,9 @@ namespace Ryujinx.Ava.UI.ViewModels
config.System.UseHypervisor.Value = UseHypervisor;
// Graphics
config.Graphics.VSyncMode.Value = VSyncMode;
config.Graphics.EnableCustomVSyncInterval.Value = EnableCustomVSyncInterval;
config.Graphics.CustomVSyncInterval.Value = CustomVSyncInterval;
config.Graphics.GraphicsBackend.Value = (GraphicsBackend)GraphicsBackendIndex;
config.Graphics.PreferredGpu.Value = _gpuIds.ElementAtOrDefault(PreferredGpuIndex);
config.Graphics.EnableShaderCache.Value = EnableShaderCache;

View File

@ -0,0 +1,14 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using System.Collections.ObjectModel;
namespace Ryujinx.Ava.UI.ViewModels
{
public partial class UserSelectorDialogViewModel : BaseModel
{
[ObservableProperty] private UserId _selectedUserId;
[ObservableProperty] private ObservableCollection<BaseModel> _profiles = [];
}
}

Some files were not shown because too many files have changed in this diff Show More