Compare commits

..

20 Commits

Author SHA1 Message Date
3a4eeb77fe headless: Change window icon size to 48x48 (#4247) 2023-01-09 18:02:41 +00:00
51b3953cfc [Headless] Add missing arguments & Fix typos (#4193)
* headless: Fix typos in command line options

* Remove nullable from command line options

Add EnableMacroHLE option
Add HideCursorOnIdle option

* headless: Adjust enable-ptc help text

* headless: Use switch statement instead of if-else chain

* headless: Improve formatting for long constructors

* headless: Remove discards from SDL_ShowCursor()

* headless: Add window icon

* Fix hiding cursor on idle

At least on Wayland, SDL2 doesn't produce any mouse motion events.

* Add new command line args: BaseDataDir and UserProfile

* headless: Read icon from embedded resource

* headless: Skip SetWindowIcon() on Windows if dll isn't present

* headless: Fix division by zero

* headless: Fix command line options not working correctly

* headless: Fix crash when viewing command line options

* headless: Load window icon bmp from memory

* Add comment to the workaround for SDL_LoadBMP_RW

* headless: Enable logging to file by default

* headless: Add 3 options for --hide-cursor

Replaces --disable-hide-cursor-on-idle
2023-01-09 04:55:37 +01:00
610eecc1c1 ava: Fixes regressions from refactoring (#4237)
* ava: Fix regressions from #4178

* Remove duplicated code

* real fix for right click menu

Co-Authored-By: Isaac Marovitz <42140194+IsaacMarovitz@users.noreply.github.com>

* Remove ContentDialogOverlay

Co-authored-by: Isaac Marovitz <42140194+IsaacMarovitz@users.noreply.github.com>
2023-01-09 03:37:20 +00:00
492056abf6 Ava: Make Avalonia use our logging system (#4231)
* Ava: Make Avalonia use our logging system

* LoggerAdapter: Address review comments

* Update Ryujinx.Common/Logging/LogClass.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>
2023-01-09 04:32:20 +01:00
ee6e682ab4 Fix selection bar (#4236) 2023-01-09 03:36:11 +01:00
6f60e102a2 HIPC: Fix reply possibly also receiving one request (#4232) 2023-01-08 15:34:49 -03:00
eeb2af9953 Ava GUI: MainWindow Refactor (#4178)
* Fix redundancies

* Add back elses

* `MainWindow` Refactor

* Switch commands to `ReflectionBinding`

Not required in Ava 11

* Update Ryujinx.Ava/AppHost.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Update Ryujinx.Ava/AppHost.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Update Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Update Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Update Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Update Ryujinx.Ava/AppHost.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Update Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Update Ryujinx.Ava/AppHost.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Update Ryujinx.Ava/AppHost.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Update Ryujinx.Ava/AppHost.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Update Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Update Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Update Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Update Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Update Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Update Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Update Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Update Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Update Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Update Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Update Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Update Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Update Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Resolve issues

* Remove Ava 11 Fix

* Update Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs

Co-authored-by: gdkchan <gab.dark.100@gmail.com>

* Update Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs

Co-authored-by: gdkchan <gab.dark.100@gmail.com>

* Fix whitespace + other suggestions

* Move Vsync colours to `Styles.xaml`

* Remove catch all

* Use `switch` instead of `if`

* Update locale keys

* Use block-scoped namespaces

* Fix improper Ava api usage then

* Static PTC

* Fix `GridItemSelectorSize` with `ShowNames`

* Update for new About Window

* Add back search fix

Co-authored-by: Ac_K <Acoustik666@gmail.com>
Co-authored-by: gdkchan <gab.dark.100@gmail.com>
2023-01-08 18:46:25 +01:00
550747eac6 Horizon: Impl Prepo, Fixes bugs, Clean things (#4220)
* Horizon: Impl Prepo, Fixes bugs, Clean things

* remove ToArray()

* resultCode > status

* Remove old services

* Addresses gdkchan's comments and more cleanup

* Addresses Gdkchan's feedback 2

* Reorganize services, make sure service are loaded before guest

Co-Authored-By: gdkchan <5624669+gdkchan@users.noreply.github.com>

* Create interfaces for lm and sm

Co-authored-by: gdkchan <5624669+gdkchan@users.noreply.github.com>
2023-01-08 12:13:39 +00:00
3ffceab1fb MainWindow: Vertically center SearchBox TextPresenter (#4223) 2023-01-07 16:01:53 +00:00
b9f2a96595 ava: Fix regression caused by #4013 (#4222)
Avalonia seems to not like when the artifact doesns't match the root namespace...

Address that by moving the binary to "Ryujinx" like we do on macOS build.
2023-01-07 12:24:21 +01:00
cbaa845f5d Include a start.sh file with correct launch options (#4013)
* Include reference to start.sh to be bundled

* Add start.sh

* Fix silly mistake I made on windows-x64

* ... I cannot read properly

* Make same changes for avalonia csproj

* Remove notice from start.sh

Co-authored-by: Mary-nyan <thog@protonmail.com>

* Update Ryujinx/Ryujinx.csproj

Co-authored-by: Mary-nyan <thog@protonmail.com>

* Update Ryujinx.Ava/Ryujinx.Ava.csproj

Co-authored-by: Mary-nyan <thog@protonmail.com>

* Update distribution/linux/start.sh

Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>

* Update distribution/linux/start.sh

Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>

* Update Ryujinx.Ava/Ryujinx.Ava.csproj

Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>

* Update Ryujinx.csproj

* Update Ryujinx.Ava.csproj

* Rename start.sh to Ryujinx.sh

* Update Ryujinx.csproj

* Update Ryujinx.Ava.csproj

* Update Ryujinx.Ava.csproj

* Update Ryujinx.Ava/Ryujinx.Ava.csproj

Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>

* Add `GDK_BACKEND` variable

* Update Ryujinx.Ava.csproj

* Update Program.cs

* Update Program.cs

* Update Ryujinx.sh

* Update Program.cs

* linux: Register mime types on launch

* Add DOTNET_EnableAlternateStackCheck=1 to desktop file

* linux: Add exclusion for RegisterMimeTypes for flathub builds

* Update logo path

* Cleanup Ryujinx.sh

* Fix typo in ReleaseInformation

* gha: Fix permissions for linux release binaries

* ava: Rename output assembly to Ryujinx

* Update mime database after installing new types

Wait until logging is available before registering mime types

* Copy mime types to output directory

Co-authored-by: Mary-nyan <thog@protonmail.com>
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
2023-01-07 09:06:13 +01:00
9e2681f2d7 Ava GUI: AboutWindow Refactor (#4196)
* Start `AboutWindow` refactor

* Redesign

* Update logos + ViewModel

* Fix GTK

* Cleanup usings

* Fix mismatched font size

* Update LocaleKeys

* Block scoped namespace

* Fix appearence on German

* Update Ryujinx.Ava/UI/ViewModels/AboutWindowViewModel.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Update Ryujinx.Ava/UI/ViewModels/AboutWindowViewModel.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Move version number up

* Move see all contributors button left

* Add a couple `\n`

* Tooltips

* Layout fix

* Update Ryujinx.Ava/UI/ViewModels/AboutWindowViewModel.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

Co-authored-by: Ac_K <Acoustik666@gmail.com>
2023-01-07 01:29:18 +01:00
81fae0d1a6 [hipc] Fix 'Unexpected result code Success returned' in Reply() (#4215)
* horizon: Add AbortOnFailureUnless()

* hipc: Replace AbortUnless() with AbortOnFailureUnless() in Reply()
2023-01-07 00:57:21 +01:00
38519f3b9a Ava GUI: SettingsWindow Refactor (#4177)
* Fix redundancies

* Add back elses

* Settings Refactor

* Fix Disposal functions

* Use `ReflectionBinding` instead of redundant funcs

* Ac_K suggestions

* Update Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Update locale keys

* Update Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs

Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>

* Update Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml.cs

Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>

* Update Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml.cs

Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>

* Update Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs

Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>

* Use block-scoped namespaces

* Fix typo

* Make `TimeZone` internal again

Co-authored-by: Ac_K <Acoustik666@gmail.com>
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
2023-01-07 00:35:21 +01:00
7f27aabbd0 chore: Update Ryujinx.SDL2-CS to 2.26.1 (#4199) 2023-01-06 17:52:25 +01:00
e876c43ce9 Misc; Remove duplicated entries and clean locale csproj (#4209) 2023-01-05 04:30:55 +01:00
8639245533 hle: Add safety measure around overflow in ScheduleFutureInvocation
Fix crash on Linux since 08831eecf7.
2023-01-05 01:55:27 +01:00
d6b86a6629 Readd Ryujinx.Ui.LocaleGenerator removed in #4188 (again) 2023-01-05 00:23:17 +01:00
8f2b7b5b8e Readd Ryujinx.Ui.LocaleGenerator removed in #4188 2023-01-05 00:20:47 +01:00
fc4b7cba2c Make PPTC state non-static (#4157)
* Make PPTC state non-static

* DiskCacheLoadState can be null
2023-01-05 00:01:44 +01:00
191 changed files with 6276 additions and 4521 deletions

View File

@ -69,6 +69,12 @@ jobs:
- name: Publish Ryujinx.Ava
run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.DOTNET_RUNTIME_IDENTIFIER }}" -o ./publish_ava -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:DebugType=embedded -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER Ryujinx.Ava --self-contained true
if: github.event_name == 'pull_request'
- name: Rename Avalonia (Windows)
run: mv ./publish_ava/Ryujinx.Ava.exe ./publish_ava/Ryujinx.exe
if: runner.os == 'Windows' && github.event_name == 'pull_request'
- name: Rename Avalonia (Unix)
run: mv ./publish_ava/Ryujinx.Ava ./publish_ava/Ryujinx
if: runner.os != 'Windows' && github.event_name == 'pull_request'
- name: Upload Ryujinx artifact
uses: actions/upload-artifact@v3
with:

View File

@ -38,19 +38,21 @@ jobs:
shell: bash
- name: Configure for release
run: |
sed -r --in-place 's/\%\%RYUJINX_BUILD_VERSION\%\%/${{ steps.version_info.outputs.build_version }}/g;' Ryujinx.Common/ReleaseInformations.cs
sed -r --in-place 's/\%\%RYUJINX_BUILD_GIT_HASH\%\%/${{ steps.version_info.outputs.git_short_hash }}/g;' Ryujinx.Common/ReleaseInformations.cs
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' Ryujinx.Common/ReleaseInformations.cs
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}/g;' Ryujinx.Common/ReleaseInformations.cs
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' Ryujinx.Common/ReleaseInformations.cs
sed -r --in-place 's/\%\%RYUJINX_BUILD_VERSION\%\%/${{ steps.version_info.outputs.build_version }}/g;' Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_BUILD_GIT_HASH\%\%/${{ steps.version_info.outputs.git_short_hash }}/g;' Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}/g;' Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' Ryujinx.Common/ReleaseInformation.cs
shell: bash
- name: Create output dir
run: "mkdir release_output"
- name: Publish Windows
run: |
dotnet publish -c Release -r win10-x64 -o ./publish_windows/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded Ryujinx --self-contained true
dotnet publish -c Release -r win10-x64 -o ./publish_windows_sdl2_headless/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded Ryujinx.Headless.SDL2 --self-contained true
dotnet publish -c Release -r win10-x64 -o ./publish_windows_ava/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded Ryujinx.Ava --self-contained true
mv ./publish_windows_ava/publish/Ryujinx.Ava.exe ./publish_windows_ava/publish/Ryujinx.exe
- name: Packing Windows builds
run: |
pushd publish_windows
@ -71,19 +73,28 @@ jobs:
dotnet publish -c Release -r linux-x64 -o ./publish_linux/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded Ryujinx --self-contained true
dotnet publish -c Release -r linux-x64 -o ./publish_linux_sdl2_headless/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded Ryujinx.Headless.SDL2 --self-contained true
dotnet publish -c Release -r linux-x64 -o ./publish_linux_ava/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded Ryujinx.Ava --self-contained true
mv ./publish_linux_ava/publish/Ryujinx.Ava ./publish_linux_ava/publish/Ryujinx
- name: Packing Linux builds
run: |
pushd publish_linux
tar -czvf ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-linux_x64.tar.gz publish
tar --exclude "publish/Ryujinx" -cvf ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-linux_x64.tar publish
python3 ../distribution/misc/add_tar_exec.py ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-linux_x64.tar "publish/Ryujinx" "publish/Ryujinx"
gzip -9 < ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-linux_x64.tar > ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-linux_x64.tar.gz
rm ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-linux_x64.tar
popd
pushd publish_linux_sdl2_headless
tar -czvf ../release_output/sdl2-ryujinx-headless-${{ steps.version_info.outputs.build_version }}-linux_x64.tar.gz publish
tar --exclude "publish/Ryujinx.Headless.SDL2" -cvf ../release_output/sdl2-ryujinx-headless-${{ steps.version_info.outputs.build_version }}-linux_x64.tar publish
python3 ../distribution/misc/add_tar_exec.py ../release_output/sdl2-ryujinx-headless-${{ steps.version_info.outputs.build_version }}-linux_x64.tar "publish/Ryujinx.Headless.SDL2" "publish/Ryujinx.Headless.SDL2"
gzip -9 < ../release_output/sdl2-ryujinx-headless-${{ steps.version_info.outputs.build_version }}-linux_x64.tar > ../release_output/sdl2-ryujinx-headless-${{ steps.version_info.outputs.build_version }}-linux_x64.tar.gz
rm ../release_output/sdl2-ryujinx-headless-${{ steps.version_info.outputs.build_version }}-linux_x64.tar
popd
pushd publish_linux_ava
tar -czvf ../release_output/test-ava-ryujinx-${{ steps.version_info.outputs.build_version }}-linux_x64.tar.gz publish
tar --exclude "publish/Ryujinx" -cvf ../release_output/test-ava-ryujinx-${{ steps.version_info.outputs.build_version }}-linux_x64.tar publish
python3 ../distribution/misc/add_tar_exec.py ../release_output/test-ava-ryujinx-${{ steps.version_info.outputs.build_version }}-linux_x64.tar "publish/Ryujinx" "publish/Ryujinx"
gzip -9 < ../release_output/test-ava-ryujinx-${{ steps.version_info.outputs.build_version }}-linux_x64.tar > ../release_output/test-ava-ryujinx-${{ steps.version_info.outputs.build_version }}-linux_x64.tar.gz
rm ../release_output/test-ava-ryujinx-${{ steps.version_info.outputs.build_version }}-linux_x64.tar
popd
shell: bash

View File

@ -6,7 +6,6 @@ using ARMeilleure.Instructions;
using ARMeilleure.IntermediateRepresentation;
using ARMeilleure.Memory;
using ARMeilleure.State;
using ARMeilleure.Translation.PTC;
using System;
using System.Collections.Generic;
using System.Reflection;
@ -44,14 +43,13 @@ namespace ARMeilleure.Translation
public IMemoryManager Memory { get; }
public bool HasPtc { get; }
public EntryTable<uint> CountTable { get; }
public AddressTable<ulong> FunctionTable { get; }
public TranslatorStubs Stubs { get; }
public ulong EntryAddress { get; }
public bool HighCq { get; }
public bool HasPtc { get; }
public Aarch32Mode Mode { get; }
private int _ifThenBlockStateIndex = 0;
@ -66,15 +64,16 @@ namespace ARMeilleure.Translation
TranslatorStubs stubs,
ulong entryAddress,
bool highCq,
bool hasPtc,
Aarch32Mode mode)
{
HasPtc = Ptc.State != PtcState.Disabled;
Memory = memory;
CountTable = countTable;
FunctionTable = funcTable;
Stubs = stubs;
EntryAddress = entryAddress;
HighCq = highCq;
HasPtc = hasPtc;
Mode = mode;
_labels = new Dictionary<ulong, Operand>();

View File

@ -0,0 +1,10 @@
using System;
namespace ARMeilleure.Translation.PTC
{
public interface IPtcLoadState
{
event Action<PtcLoadingState, int, int> PtcStateChanged;
void Continue();
}
}

View File

@ -22,7 +22,7 @@ using static ARMeilleure.Translation.PTC.PtcFormatter;
namespace ARMeilleure.Translation.PTC
{
public static class Ptc
class Ptc : IPtcLoadState
{
private const string OuterHeaderMagicString = "PTCohd\0\0";
private const string InnerHeaderMagicString = "PTCihd\0\0";
@ -35,45 +35,49 @@ namespace ARMeilleure.Translation.PTC
private const string TitleIdTextDefault = "0000000000000000";
private const string DisplayVersionDefault = "0";
internal static readonly Symbol PageTableSymbol = new(SymbolType.Special, 1);
internal static readonly Symbol CountTableSymbol = new(SymbolType.Special, 2);
internal static readonly Symbol DispatchStubSymbol = new(SymbolType.Special, 3);
public static readonly Symbol PageTableSymbol = new(SymbolType.Special, 1);
public static readonly Symbol CountTableSymbol = new(SymbolType.Special, 2);
public static readonly Symbol DispatchStubSymbol = new(SymbolType.Special, 3);
private const byte FillingByte = 0x00;
private const CompressionLevel SaveCompressionLevel = CompressionLevel.Fastest;
public PtcProfiler Profiler { get; }
// Carriers.
private static MemoryStream _infosStream;
private static List<byte[]> _codesList;
private static MemoryStream _relocsStream;
private static MemoryStream _unwindInfosStream;
private MemoryStream _infosStream;
private List<byte[]> _codesList;
private MemoryStream _relocsStream;
private MemoryStream _unwindInfosStream;
private static readonly ulong _outerHeaderMagic;
private static readonly ulong _innerHeaderMagic;
private readonly ulong _outerHeaderMagic;
private readonly ulong _innerHeaderMagic;
private static readonly ManualResetEvent _waitEvent;
private readonly ManualResetEvent _waitEvent;
private static readonly object _lock;
private readonly object _lock;
private static bool _disposed;
private bool _disposed;
internal static string TitleIdText { get; private set; }
internal static string DisplayVersion { get; private set; }
public string TitleIdText { get; private set; }
public string DisplayVersion { get; private set; }
private static MemoryManagerMode _memoryMode;
private MemoryManagerType _memoryMode;
internal static string CachePathActual { get; private set; }
internal static string CachePathBackup { get; private set; }
public string CachePathActual { get; private set; }
public string CachePathBackup { get; private set; }
internal static PtcState State { get; private set; }
public PtcState State { get; private set; }
// Progress reporting helpers.
private static volatile int _translateCount;
private static volatile int _translateTotalCount;
public static event Action<PtcLoadingState, int, int> PtcStateChanged;
private volatile int _translateCount;
private volatile int _translateTotalCount;
public event Action<PtcLoadingState, int, int> PtcStateChanged;
static Ptc()
public Ptc()
{
Profiler = new PtcProfiler(this);
InitializeCarriers();
_outerHeaderMagic = BinaryPrimitives.ReadUInt64LittleEndian(EncodingCache.UTF8NoBOM.GetBytes(OuterHeaderMagicString).AsSpan());
@ -94,12 +98,12 @@ namespace ARMeilleure.Translation.PTC
Disable();
}
public static void Initialize(string titleIdText, string displayVersion, bool enabled, MemoryManagerMode memoryMode)
public void Initialize(string titleIdText, string displayVersion, bool enabled, MemoryManagerType memoryMode)
{
Wait();
PtcProfiler.Wait();
PtcProfiler.ClearEntries();
Profiler.Wait();
Profiler.ClearEntries();
Logger.Info?.Print(LogClass.Ptc, $"Initializing Profiled Persistent Translation Cache (enabled: {enabled}).");
@ -137,12 +141,12 @@ namespace ARMeilleure.Translation.PTC
CachePathBackup = Path.Combine(workPathBackup, DisplayVersion);
PreLoad();
PtcProfiler.PreLoad();
Profiler.PreLoad();
Enable();
}
private static void InitializeCarriers()
private void InitializeCarriers()
{
_infosStream = new MemoryStream();
_codesList = new List<byte[]>();
@ -150,7 +154,7 @@ namespace ARMeilleure.Translation.PTC
_unwindInfosStream = new MemoryStream();
}
private static void DisposeCarriers()
private void DisposeCarriers()
{
_infosStream.Dispose();
_codesList.Clear();
@ -158,12 +162,12 @@ namespace ARMeilleure.Translation.PTC
_unwindInfosStream.Dispose();
}
private static bool AreCarriersEmpty()
private bool AreCarriersEmpty()
{
return _infosStream.Length == 0L && _codesList.Count == 0 && _relocsStream.Length == 0L && _unwindInfosStream.Length == 0L;
}
private static void ResetCarriersIfNeeded()
private void ResetCarriersIfNeeded()
{
if (AreCarriersEmpty())
{
@ -175,7 +179,7 @@ namespace ARMeilleure.Translation.PTC
InitializeCarriers();
}
private static void PreLoad()
private void PreLoad()
{
string fileNameActual = string.Concat(CachePathActual, ".cache");
string fileNameBackup = string.Concat(CachePathBackup, ".cache");
@ -199,7 +203,7 @@ namespace ARMeilleure.Translation.PTC
}
}
private static unsafe bool Load(string fileName, bool isBackup)
private unsafe bool Load(string fileName, bool isBackup)
{
using (FileStream compressedStream = new(fileName, FileMode.Open))
using (DeflateStream deflateStream = new(compressedStream, CompressionMode.Decompress, true))
@ -376,12 +380,12 @@ namespace ARMeilleure.Translation.PTC
return true;
}
private static void InvalidateCompressedStream(FileStream compressedStream)
private void InvalidateCompressedStream(FileStream compressedStream)
{
compressedStream.SetLength(0L);
}
private static void PreSave()
private void PreSave()
{
_waitEvent.Reset();
@ -409,7 +413,7 @@ namespace ARMeilleure.Translation.PTC
_waitEvent.Set();
}
private static unsafe void Save(string fileName)
private unsafe void Save(string fileName)
{
int translatedFuncsCount;
@ -517,7 +521,7 @@ namespace ARMeilleure.Translation.PTC
}
}
internal static void LoadTranslations(Translator translator)
public void LoadTranslations(Translator translator)
{
if (AreCarriersEmpty())
{
@ -550,7 +554,7 @@ namespace ARMeilleure.Translation.PTC
bool isEntryChanged = infoEntry.Hash != ComputeHash(translator.Memory, infoEntry.Address, infoEntry.GuestSize);
if (isEntryChanged || (!infoEntry.HighCq && PtcProfiler.ProfiledFuncs.TryGetValue(infoEntry.Address, out var value) && value.HighCq))
if (isEntryChanged || (!infoEntry.HighCq && Profiler.ProfiledFuncs.TryGetValue(infoEntry.Address, out var value) && value.HighCq))
{
infoEntry.Stubbed = true;
infoEntry.CodeLength = 0;
@ -601,38 +605,38 @@ namespace ARMeilleure.Translation.PTC
Logger.Info?.Print(LogClass.Ptc, $"{translator.Functions.Count} translated functions loaded");
}
private static int GetEntriesCount()
private int GetEntriesCount()
{
return _codesList.Count;
}
[Conditional("DEBUG")]
private static void SkipCode(int index, int codeLength)
private void SkipCode(int index, int codeLength)
{
Debug.Assert(_codesList[index].Length == 0);
Debug.Assert(codeLength == 0);
}
private static void SkipReloc(int relocEntriesCount)
private void SkipReloc(int relocEntriesCount)
{
_relocsStream.Seek(relocEntriesCount * RelocEntry.Stride, SeekOrigin.Current);
}
private static void SkipUnwindInfo(BinaryReader unwindInfosReader)
private void SkipUnwindInfo(BinaryReader unwindInfosReader)
{
int pushEntriesLength = unwindInfosReader.ReadInt32();
_unwindInfosStream.Seek(pushEntriesLength * UnwindPushEntry.Stride + UnwindInfo.Stride, SeekOrigin.Current);
}
private static byte[] ReadCode(int index, int codeLength)
private byte[] ReadCode(int index, int codeLength)
{
Debug.Assert(_codesList[index].Length == codeLength);
return _codesList[index];
}
private static RelocEntry[] GetRelocEntries(BinaryReader relocsReader, int relocEntriesCount)
private RelocEntry[] GetRelocEntries(BinaryReader relocsReader, int relocEntriesCount)
{
RelocEntry[] relocEntries = new RelocEntry[relocEntriesCount];
@ -648,7 +652,7 @@ namespace ARMeilleure.Translation.PTC
return relocEntries;
}
private static void PatchCode(Translator translator, Span<byte> code, RelocEntry[] relocEntries, out Counter<uint> callCounter)
private void PatchCode(Translator translator, Span<byte> code, RelocEntry[] relocEntries, out Counter<uint> callCounter)
{
callCounter = null;
@ -702,7 +706,7 @@ namespace ARMeilleure.Translation.PTC
}
}
private static UnwindInfo ReadUnwindInfo(BinaryReader unwindInfosReader)
private UnwindInfo ReadUnwindInfo(BinaryReader unwindInfosReader)
{
int pushEntriesLength = unwindInfosReader.ReadInt32();
@ -723,7 +727,7 @@ namespace ARMeilleure.Translation.PTC
return new UnwindInfo(pushEntries, prologueSize);
}
private static TranslatedFunction FastTranslate(
private TranslatedFunction FastTranslate(
byte[] code,
Counter<uint> callCounter,
ulong guestSize,
@ -736,19 +740,19 @@ namespace ARMeilleure.Translation.PTC
return new TranslatedFunction(gFunc, callCounter, guestSize, highCq);
}
private static void UpdateInfo(InfoEntry infoEntry)
private void UpdateInfo(InfoEntry infoEntry)
{
_infosStream.Seek(-Unsafe.SizeOf<InfoEntry>(), SeekOrigin.Current);
SerializeStructure(_infosStream, infoEntry);
}
private static void StubCode(int index)
private void StubCode(int index)
{
_codesList[index] = Array.Empty<byte>();
}
private static void StubReloc(int relocEntriesCount)
private void StubReloc(int relocEntriesCount)
{
for (int i = 0; i < relocEntriesCount * RelocEntry.Stride; i++)
{
@ -756,7 +760,7 @@ namespace ARMeilleure.Translation.PTC
}
}
private static void StubUnwindInfo(BinaryReader unwindInfosReader)
private void StubUnwindInfo(BinaryReader unwindInfosReader)
{
int pushEntriesLength = unwindInfosReader.ReadInt32();
@ -766,9 +770,9 @@ namespace ARMeilleure.Translation.PTC
}
}
internal static void MakeAndSaveTranslations(Translator translator)
public void MakeAndSaveTranslations(Translator translator)
{
var profiledFuncsToTranslate = PtcProfiler.GetProfiledFuncsToTranslate(translator.Functions);
var profiledFuncsToTranslate = Profiler.GetProfiledFuncsToTranslate(translator.Functions);
_translateCount = 0;
_translateTotalCount = profiledFuncsToTranslate.Count;
@ -811,7 +815,7 @@ namespace ARMeilleure.Translation.PTC
{
ulong address = item.address;
Debug.Assert(PtcProfiler.IsAddressInStaticCodeRange(address));
Debug.Assert(Profiler.IsAddressInStaticCodeRange(address));
TranslatedFunction func = translator.Translate(address, item.funcProfile.Mode, item.funcProfile.HighCq);
@ -861,7 +865,7 @@ namespace ARMeilleure.Translation.PTC
preSaveThread.Start();
}
private static void ReportProgress(object state)
private void ReportProgress(object state)
{
const int refreshRate = 50; // ms.
@ -882,12 +886,12 @@ namespace ARMeilleure.Translation.PTC
while (!endEvent.WaitOne(refreshRate));
}
internal static Hash128 ComputeHash(IMemoryManager memory, ulong address, ulong guestSize)
public static Hash128 ComputeHash(IMemoryManager memory, ulong address, ulong guestSize)
{
return XXHash128.ComputeHash(memory.GetSpan(address, checked((int)(guestSize))));
}
internal static void WriteCompiledFunction(ulong address, ulong guestSize, Hash128 hash, bool highCq, CompiledFunction compiledFunc)
public void WriteCompiledFunction(ulong address, ulong guestSize, Hash128 hash, bool highCq, CompiledFunction compiledFunc)
{
lock (_lock)
{
@ -936,12 +940,12 @@ namespace ARMeilleure.Translation.PTC
}
}
private static void WriteCode(ReadOnlySpan<byte> code)
private void WriteCode(ReadOnlySpan<byte> code)
{
_codesList.Add(code.ToArray());
}
internal static bool GetEndianness()
public static bool GetEndianness()
{
return BitConverter.IsLittleEndian;
}
@ -955,7 +959,7 @@ namespace ARMeilleure.Translation.PTC
(uint)HardwareCapabilities.FeatureInfo7Ecx);
}
private static byte GetMemoryManagerMode()
private byte GetMemoryManagerMode()
{
return (byte)_memoryMode;
}
@ -1050,12 +1054,12 @@ namespace ARMeilleure.Translation.PTC
public int RelocEntriesCount;
}
private static void Enable()
private void Enable()
{
State = PtcState.Enabled;
}
public static void Continue()
public void Continue()
{
if (State == PtcState.Enabled)
{
@ -1063,7 +1067,7 @@ namespace ARMeilleure.Translation.PTC
}
}
public static void Close()
public void Close()
{
if (State == PtcState.Enabled ||
State == PtcState.Continuing)
@ -1072,17 +1076,17 @@ namespace ARMeilleure.Translation.PTC
}
}
internal static void Disable()
public void Disable()
{
State = PtcState.Disabled;
}
private static void Wait()
private void Wait()
{
_waitEvent.WaitOne();
}
public static void Dispose()
public void Dispose()
{
if (!_disposed)
{

View File

@ -16,7 +16,7 @@ using static ARMeilleure.Translation.PTC.PtcFormatter;
namespace ARMeilleure.Translation.PTC
{
public static class PtcProfiler
class PtcProfiler
{
private const string OuterHeaderMagicString = "Pohd\0\0\0\0";
@ -26,27 +26,31 @@ namespace ARMeilleure.Translation.PTC
private const CompressionLevel SaveCompressionLevel = CompressionLevel.Fastest;
private static readonly System.Timers.Timer _timer;
private readonly Ptc _ptc;
private static readonly ulong _outerHeaderMagic;
private readonly System.Timers.Timer _timer;
private static readonly ManualResetEvent _waitEvent;
private readonly ulong _outerHeaderMagic;
private static readonly object _lock;
private readonly ManualResetEvent _waitEvent;
private static bool _disposed;
private readonly object _lock;
private static Hash128 _lastHash;
private bool _disposed;
internal static Dictionary<ulong, FuncProfile> ProfiledFuncs { get; private set; }
private Hash128 _lastHash;
internal static bool Enabled { get; private set; }
public Dictionary<ulong, FuncProfile> ProfiledFuncs { get; private set; }
public static ulong StaticCodeStart { internal get; set; }
public static ulong StaticCodeSize { internal get; set; }
public bool Enabled { get; private set; }
static PtcProfiler()
public ulong StaticCodeStart { get; set; }
public ulong StaticCodeSize { get; set; }
public PtcProfiler(Ptc ptc)
{
_ptc = ptc;
_timer = new System.Timers.Timer((double)SaveInterval * 1000d);
_timer.Elapsed += PreSave;
@ -63,7 +67,7 @@ namespace ARMeilleure.Translation.PTC
Enabled = false;
}
internal static void AddEntry(ulong address, ExecutionMode mode, bool highCq)
public void AddEntry(ulong address, ExecutionMode mode, bool highCq)
{
if (IsAddressInStaticCodeRange(address))
{
@ -76,7 +80,7 @@ namespace ARMeilleure.Translation.PTC
}
}
internal static void UpdateEntry(ulong address, ExecutionMode mode, bool highCq)
public void UpdateEntry(ulong address, ExecutionMode mode, bool highCq)
{
if (IsAddressInStaticCodeRange(address))
{
@ -91,12 +95,12 @@ namespace ARMeilleure.Translation.PTC
}
}
internal static bool IsAddressInStaticCodeRange(ulong address)
public bool IsAddressInStaticCodeRange(ulong address)
{
return address >= StaticCodeStart && address < StaticCodeStart + StaticCodeSize;
}
internal static ConcurrentQueue<(ulong address, FuncProfile funcProfile)> GetProfiledFuncsToTranslate(TranslatorCache<TranslatedFunction> funcs)
public ConcurrentQueue<(ulong address, FuncProfile funcProfile)> GetProfiledFuncsToTranslate(TranslatorCache<TranslatedFunction> funcs)
{
var profiledFuncsToTranslate = new ConcurrentQueue<(ulong address, FuncProfile funcProfile)>();
@ -111,18 +115,18 @@ namespace ARMeilleure.Translation.PTC
return profiledFuncsToTranslate;
}
internal static void ClearEntries()
public void ClearEntries()
{
ProfiledFuncs.Clear();
ProfiledFuncs.TrimExcess();
}
internal static void PreLoad()
public void PreLoad()
{
_lastHash = default;
string fileNameActual = string.Concat(Ptc.CachePathActual, ".info");
string fileNameBackup = string.Concat(Ptc.CachePathBackup, ".info");
string fileNameActual = string.Concat(_ptc.CachePathActual, ".info");
string fileNameBackup = string.Concat(_ptc.CachePathBackup, ".info");
FileInfo fileInfoActual = new FileInfo(fileNameActual);
FileInfo fileInfoBackup = new FileInfo(fileNameBackup);
@ -143,7 +147,7 @@ namespace ARMeilleure.Translation.PTC
}
}
private static bool Load(string fileName, bool isBackup)
private bool Load(string fileName, bool isBackup)
{
using (FileStream compressedStream = new(fileName, FileMode.Open))
using (DeflateStream deflateStream = new(compressedStream, CompressionMode.Decompress, true))
@ -228,22 +232,22 @@ namespace ARMeilleure.Translation.PTC
return DeserializeDictionary<ulong, FuncProfile>(stream, (stream) => DeserializeStructure<FuncProfile>(stream));
}
private static ReadOnlySpan<byte> GetReadOnlySpan(MemoryStream memoryStream)
private ReadOnlySpan<byte> GetReadOnlySpan(MemoryStream memoryStream)
{
return new(memoryStream.GetBuffer(), (int)memoryStream.Position, (int)memoryStream.Length - (int)memoryStream.Position);
}
private static void InvalidateCompressedStream(FileStream compressedStream)
private void InvalidateCompressedStream(FileStream compressedStream)
{
compressedStream.SetLength(0L);
}
private static void PreSave(object source, System.Timers.ElapsedEventArgs e)
private void PreSave(object source, System.Timers.ElapsedEventArgs e)
{
_waitEvent.Reset();
string fileNameActual = string.Concat(Ptc.CachePathActual, ".info");
string fileNameBackup = string.Concat(Ptc.CachePathBackup, ".info");
string fileNameActual = string.Concat(_ptc.CachePathActual, ".info");
string fileNameBackup = string.Concat(_ptc.CachePathBackup, ".info");
FileInfo fileInfoActual = new FileInfo(fileNameActual);
@ -257,7 +261,7 @@ namespace ARMeilleure.Translation.PTC
_waitEvent.Set();
}
private static void Save(string fileName)
private void Save(string fileName)
{
int profiledFuncsCount;
@ -329,7 +333,7 @@ namespace ARMeilleure.Translation.PTC
}
}
private static void Serialize(Stream stream, Dictionary<ulong, FuncProfile> profiledFuncs)
private void Serialize(Stream stream, Dictionary<ulong, FuncProfile> profiledFuncs)
{
SerializeDictionary(stream, profiledFuncs, (stream, structure) => SerializeStructure(stream, structure));
}
@ -361,7 +365,7 @@ namespace ARMeilleure.Translation.PTC
}
[StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 5*/)]
internal struct FuncProfile
public struct FuncProfile
{
public ExecutionMode Mode;
public bool HighCq;
@ -373,10 +377,10 @@ namespace ARMeilleure.Translation.PTC
}
}
internal static void Start()
public void Start()
{
if (Ptc.State == PtcState.Enabled ||
Ptc.State == PtcState.Continuing)
if (_ptc.State == PtcState.Enabled ||
_ptc.State == PtcState.Continuing)
{
Enabled = true;
@ -384,7 +388,7 @@ namespace ARMeilleure.Translation.PTC
}
}
public static void Stop()
public void Stop()
{
Enabled = false;
@ -394,12 +398,12 @@ namespace ARMeilleure.Translation.PTC
}
}
internal static void Wait()
public void Wait()
{
_waitEvent.WaitOne();
}
public static void Dispose()
public void Dispose()
{
if (!_disposed)
{

View File

@ -44,6 +44,8 @@ namespace ARMeilleure.Translation
private readonly IJitMemoryAllocator _allocator;
private readonly ConcurrentQueue<KeyValuePair<ulong, TranslatedFunction>> _oldFuncs;
private readonly Ptc _ptc;
internal TranslatorCache<TranslatedFunction> Functions { get; }
internal AddressTable<ulong> FunctionTable { get; }
internal EntryTable<uint> CountTable { get; }
@ -63,6 +65,8 @@ namespace ARMeilleure.Translation
_oldFuncs = new ConcurrentQueue<KeyValuePair<ulong, TranslatedFunction>>();
_ptc = new Ptc();
Queue = new TranslatorQueue();
JitCache.Initialize(allocator);
@ -80,22 +84,37 @@ namespace ARMeilleure.Translation
}
}
public IPtcLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled)
{
_ptc.Initialize(titleIdText, displayVersion, enabled, Memory.Type);
return _ptc;
}
public void PrepareCodeRange(ulong address, ulong size)
{
if (_ptc.Profiler.StaticCodeSize == 0)
{
_ptc.Profiler.StaticCodeStart = address;
_ptc.Profiler.StaticCodeSize = size;
}
}
public void Execute(State.ExecutionContext context, ulong address)
{
if (Interlocked.Increment(ref _threadCount) == 1)
{
IsReadyForTranslation.WaitOne();
if (Ptc.State == PtcState.Enabled)
if (_ptc.State == PtcState.Enabled)
{
Debug.Assert(Functions.Count == 0);
Ptc.LoadTranslations(this);
Ptc.MakeAndSaveTranslations(this);
_ptc.LoadTranslations(this);
_ptc.MakeAndSaveTranslations(this);
}
PtcProfiler.Start();
_ptc.Profiler.Start();
Ptc.Disable();
_ptc.Disable();
// Simple heuristic, should be user configurable in future. (1 for 4 core/ht or less, 2 for 6 core + ht
// etc). All threads are normal priority except from the last, which just fills as much of the last core
@ -148,6 +167,12 @@ namespace ARMeilleure.Translation
Stubs.Dispose();
FunctionTable.Dispose();
CountTable.Dispose();
_ptc.Close();
_ptc.Profiler.Stop();
_ptc.Dispose();
_ptc.Profiler.Dispose();
}
}
@ -189,9 +214,9 @@ namespace ARMeilleure.Translation
func = oldFunc;
}
if (PtcProfiler.Enabled)
if (_ptc.Profiler.Enabled)
{
PtcProfiler.AddEntry(address, mode, highCq: false);
_ptc.Profiler.AddEntry(address, mode, highCq: false);
}
RegisterFunction(address, func);
@ -217,6 +242,7 @@ namespace ARMeilleure.Translation
Stubs,
address,
highCq,
_ptc.State != PtcState.Disabled,
mode: Aarch32Mode.User);
Logger.StartPass(PassName.Decoding);
@ -262,7 +288,7 @@ namespace ARMeilleure.Translation
{
Hash128 hash = Ptc.ComputeHash(Memory, address, funcSize);
Ptc.WriteCompiledFunction(address, funcSize, hash, highCq, compiledFunc);
_ptc.WriteCompiledFunction(address, funcSize, hash, highCq, compiledFunc);
}
GuestFunction func = compiledFunc.Map<GuestFunction>();
@ -284,9 +310,9 @@ namespace ARMeilleure.Translation
return func;
});
if (PtcProfiler.Enabled)
if (_ptc.Profiler.Enabled)
{
PtcProfiler.UpdateEntry(request.Address, request.Mode, highCq: true);
_ptc.Profiler.UpdateEntry(request.Address, request.Mode, highCq: true);
}
RegisterFunction(request.Address, func);

View File

@ -34,7 +34,7 @@
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies" Version="5.0.1-build13" />
<PackageVersion Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Version="1.2.0" />
<PackageVersion Include="Ryujinx.GtkSharp" Version="3.24.24.59-ryujinx" />
<PackageVersion Include="Ryujinx.SDL2-CS" Version="2.24.2-build21" />
<PackageVersion Include="Ryujinx.SDL2-CS" Version="2.26.1-build23" />
<PackageVersion Include="shaderc.net" Version="0.1.0" />
<PackageVersion Include="SharpZipLib" Version="1.4.1" />
<PackageVersion Include="Silk.NET.Vulkan" Version="2.16.0" />
@ -50,7 +50,5 @@
<PackageVersion Include="System.Net.NameResolution" Version="4.3.0" />
<PackageVersion Include="System.Threading.ThreadPool" Version="4.3.0" />
<PackageVersion Include="XamlNameReferenceGenerator" Version="1.5.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3"/>
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" />
</ItemGroup>
</Project>
</Project>

View File

@ -1,5 +1,6 @@
using ARMeilleure.Translation;
using ARMeilleure.Translation.PTC;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Input;
using Avalonia.Threading;
using LibHac.Tools.FsSystem;
@ -14,6 +15,7 @@ using Ryujinx.Ava.Input;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
@ -43,7 +45,7 @@ using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Image = SixLabors.ImageSharp.Image;
using InputManager = Ryujinx.Input.HLE.InputManager;
using Key = Ryujinx.Input.Key;
using MouseButton = Ryujinx.Input.MouseButton;
@ -68,7 +70,7 @@ namespace Ryujinx.Ava
private readonly AccountManager _accountManager;
private readonly UserChannelPersistence _userChannelPersistence;
private readonly InputManager _inputManager;
private readonly MainWindow _parent;
private readonly MainWindowViewModel _viewModel;
private readonly IKeyboard _keyboardInterface;
private readonly GraphicsDebugLevel _glLogLevel;
@ -111,6 +113,7 @@ namespace Ryujinx.Ava
public bool ScreenshotRequested { get; set; }
private object _lockObject = new();
private TopLevel _topLevel;
public AppHost(
RendererHost renderer,
@ -120,9 +123,10 @@ namespace Ryujinx.Ava
ContentManager contentManager,
AccountManager accountManager,
UserChannelPersistence userChannelPersistence,
MainWindow parent)
MainWindowViewModel viewmodel,
TopLevel topLevel)
{
_parent = parent;
_viewModel = viewmodel;
_inputManager = inputManager;
_accountManager = accountManager;
_userChannelPersistence = userChannelPersistence;
@ -130,7 +134,8 @@ namespace Ryujinx.Ava
_hideCursorOnIdle = ConfigurationState.Instance.HideCursorOnIdle;
_lastCursorMoveTime = Stopwatch.GetTimestamp();
_glLogLevel = ConfigurationState.Instance.Logger.GraphicsDebugLevel;
_inputManager.SetMouseDriver(new AvaloniaMouseDriver(_parent, renderer));
_topLevel = topLevel;
_inputManager.SetMouseDriver(new AvaloniaMouseDriver(_topLevel, renderer));
_keyboardInterface = (IKeyboard)_inputManager.KeyboardDriver.GetGamepad("0");
NpadManager = _inputManager.CreateNpadManager();
@ -145,15 +150,15 @@ namespace Ryujinx.Ava
if (ApplicationPath.StartsWith("@SystemContent"))
{
ApplicationPath = _parent.VirtualFileSystem.SwitchPathToSystemPath(ApplicationPath);
ApplicationPath = _viewModel.VirtualFileSystem.SwitchPathToSystemPath(ApplicationPath);
_isFirmwareTitle = true;
}
ConfigurationState.Instance.HideCursorOnIdle.Event += HideCursorState_Changed;
_parent.PointerLeave += Parent_PointerLeft;
_parent.PointerMoved += Parent_PointerMoved;
_topLevel.PointerLeave += TopLevel_PointerLeave;
_topLevel.PointerMoved += TopLevel_PointerMoved;
ConfigurationState.Instance.System.IgnoreMissingServices.Event += UpdateIgnoreMissingServicesState;
ConfigurationState.Instance.Graphics.AspectRatio.Event += UpdateAspectRatioState;
@ -163,25 +168,27 @@ namespace Ryujinx.Ava
_gpuCancellationTokenSource = new CancellationTokenSource();
}
private void Parent_PointerMoved(object sender, PointerEventArgs e)
private void TopLevel_PointerMoved(object sender, PointerEventArgs e)
{
_lastCursorMoveTime = Stopwatch.GetTimestamp();
var p = e.GetCurrentPoint(_parent).Position;
var r = _parent.InputHitTest(p);
_isMouseInRenderer = r == Renderer;
if (sender is Control visual)
{
_lastCursorMoveTime = Stopwatch.GetTimestamp();
var point = e.GetCurrentPoint(visual).Position;
_isMouseInRenderer = Equals(visual.InputHitTest(point), Renderer);
}
}
private void Parent_PointerLeft(object sender, PointerEventArgs e)
private void TopLevel_PointerLeave(object sender, PointerEventArgs e)
{
_isMouseInRenderer = false;
_parent.Cursor = Cursor.Default;
_viewModel.Cursor = Cursor.Default;
}
private void SetRendererWindowSize(Size size)
{
if (_renderer != null)
{
double scale = _parent.PlatformImpl.RenderScaling;
double scale = _topLevel.PlatformImpl.RenderScaling;
_renderer.Window?.SetSize((int)(size.Width * scale), (int)(size.Height * scale));
}
}
@ -257,7 +264,7 @@ namespace Ryujinx.Ava
NpadManager.Initialize(Device, ConfigurationState.Instance.Hid.InputConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse);
TouchScreenManager.Initialize(Device);
_parent.ViewModel.IsGameRunning = true;
_viewModel.IsGameRunning = true;
string titleNameSection = string.IsNullOrWhiteSpace(Device.Application.TitleName)
? string.Empty
@ -277,10 +284,10 @@ namespace Ryujinx.Ava
Dispatcher.UIThread.InvokeAsync(() =>
{
_parent.Title = $"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}";
_viewModel.Title = $"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}";
});
_parent.ViewModel.HandleShaderProgress(Device);
_viewModel.SetUIProgressHandlers(Device);
Renderer.SizeChanged += Window_SizeChanged;
@ -288,7 +295,7 @@ namespace Ryujinx.Ava
_renderingThread.Start();
_parent.ViewModel.Volume = ConfigurationState.Instance.System.AudioVolume.Value;
_viewModel.Volume = ConfigurationState.Instance.System.AudioVolume.Value;
MainLoop();
@ -322,7 +329,7 @@ namespace Ryujinx.Ava
Dispatcher.UIThread.Post(() =>
{
var value = e.NewValue;
_parent.ViewModel.Volume = e.NewValue;
_viewModel.Volume = e.NewValue;
});
}
@ -357,8 +364,6 @@ namespace Ryujinx.Ava
DisplaySleep.Restore();
Ptc.Close();
PtcProfiler.Stop();
NpadManager.Dispose();
TouchScreenManager.Dispose();
Device.Dispose();
@ -372,7 +377,7 @@ namespace Ryujinx.Ava
{
if (Device.Application != null)
{
_parent.UpdateGameMetadata(Device.Application.TitleIdText);
_viewModel.UpdateGameMetadata(Device.Application.TitleIdText);
}
ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState;
@ -380,6 +385,9 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.System.EnableDockedMode.Event -= UpdateDockedModeState;
ConfigurationState.Instance.System.AudioVolume.Event -= UpdateAudioVolumeState;
_topLevel.PointerLeave -= TopLevel_PointerLeave;
_topLevel.PointerMoved -= TopLevel_PointerMoved;
_gpuCancellationTokenSource.Cancel();
_gpuCancellationTokenSource.Dispose();
@ -413,7 +421,7 @@ namespace Ryujinx.Ava
}
else
{
_parent.Cursor = Cursor.Default;
_viewModel.Cursor = Cursor.Default;
}
});
}
@ -425,57 +433,65 @@ namespace Ryujinx.Ava
SystemVersion firmwareVersion = ContentManager.GetCurrentFirmwareVersion();
if (!SetupValidator.CanStartApplication(ContentManager, ApplicationPath, out UserError userError))
if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
if (SetupValidator.CanFixStartApplication(ContentManager, ApplicationPath, userError, out firmwareVersion))
if (!SetupValidator.CanStartApplication(ContentManager, ApplicationPath, out UserError userError))
{
if (userError == UserError.NoFirmware)
{
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
LocaleManager.Instance[LocaleKeys.DialogFirmwareNoFirmwareInstalledMessage],
string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallEmbeddedMessage], firmwareVersion.VersionString),
LocaleManager.Instance[LocaleKeys.InputDialogYes],
LocaleManager.Instance[LocaleKeys.InputDialogNo],
"");
if (result != UserResult.Yes)
if (SetupValidator.CanFixStartApplication(ContentManager, ApplicationPath, userError, out firmwareVersion))
{
await UserErrorDialog.ShowUserErrorDialog(userError, _parent);
if (userError == UserError.NoFirmware)
{
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
LocaleManager.Instance[LocaleKeys.DialogFirmwareNoFirmwareInstalledMessage],
string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallEmbeddedMessage],
firmwareVersion.VersionString),
LocaleManager.Instance[LocaleKeys.InputDialogYes],
LocaleManager.Instance[LocaleKeys.InputDialogNo],
"");
if (result != UserResult.Yes)
{
await UserErrorDialog.ShowUserErrorDialog(userError, (desktop.MainWindow as MainWindow));
Device.Dispose();
return false;
}
}
if (!SetupValidator.TryFixStartApplication(ContentManager, ApplicationPath, userError, out _))
{
await UserErrorDialog.ShowUserErrorDialog(userError, (desktop.MainWindow as MainWindow));
Device.Dispose();
return false;
}
// Tell the user that we installed a firmware for them.
if (userError == UserError.NoFirmware)
{
firmwareVersion = ContentManager.GetCurrentFirmwareVersion();
_viewModel.RefreshFirmwareStatus();
await ContentDialogHelper.CreateInfoDialog(
string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstalledMessage],
firmwareVersion.VersionString),
string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallEmbeddedSuccessMessage],
firmwareVersion.VersionString),
LocaleManager.Instance[LocaleKeys.InputDialogOk],
"",
LocaleManager.Instance[LocaleKeys.RyujinxInfo]);
}
}
else
{
await UserErrorDialog.ShowUserErrorDialog(userError, (desktop.MainWindow as MainWindow));
Device.Dispose();
return false;
}
}
if (!SetupValidator.TryFixStartApplication(ContentManager, ApplicationPath, userError, out _))
{
await UserErrorDialog.ShowUserErrorDialog(userError, _parent);
Device.Dispose();
return false;
}
// Tell the user that we installed a firmware for them.
if (userError == UserError.NoFirmware)
{
firmwareVersion = ContentManager.GetCurrentFirmwareVersion();
_parent.RefreshFirmwareStatus();
await ContentDialogHelper.CreateInfoDialog(
string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstalledMessage], firmwareVersion.VersionString),
string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallEmbeddedSuccessMessage], firmwareVersion.VersionString),
LocaleManager.Instance[LocaleKeys.InputDialogOk],
"",
LocaleManager.Instance[LocaleKeys.RyujinxInfo]);
}
}
else
{
await UserErrorDialog.ShowUserErrorDialog(userError, _parent);
Device.Dispose();
return false;
}
}
@ -570,7 +586,7 @@ namespace Ryujinx.Ava
DiscordIntegrationModule.SwitchToPlayingState(Device.Application.TitleIdText, Device.Application.TitleName);
_parent.ApplicationLibrary.LoadAndSaveMetaData(Device.Application.TitleIdText, appMetadata =>
_viewModel.ApplicationLibrary.LoadAndSaveMetaData(Device.Application.TitleIdText, appMetadata =>
{
appMetadata.LastPlayed = DateTime.UtcNow.ToString();
});
@ -581,13 +597,13 @@ namespace Ryujinx.Ava
internal void Resume()
{
Device?.System.TogglePauseEmulation(false);
_parent.ViewModel.IsPaused = false;
_viewModel.IsPaused = false;
}
internal void Pause()
{
Device?.System.TogglePauseEmulation(true);
_parent.ViewModel.IsPaused = true;
_viewModel.IsPaused = true;
}
private void InitializeSwitchInstance()
@ -635,7 +651,7 @@ namespace Ryujinx.Ava
Logger.Warning?.Print(LogClass.Audio, "Found OpenAL, changing configuration.");
ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.OpenAl;
MainWindow.SaveConfig();
MainWindowViewModel.SaveConfig();
deviceDriver = new OpenALHardwareDeviceDriver();
}
@ -648,7 +664,7 @@ namespace Ryujinx.Ava
Logger.Warning?.Print(LogClass.Audio, "Found SoundIO, changing configuration.");
ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.SoundIo;
MainWindow.SaveConfig();
MainWindowViewModel.SaveConfig();
deviceDriver = new SoundIoHardwareDeviceDriver();
}
@ -674,7 +690,7 @@ namespace Ryujinx.Ava
Logger.Warning?.Print(LogClass.Audio, "Found SDL2, changing configuration.");
ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.SDL2;
MainWindow.SaveConfig();
MainWindowViewModel.SaveConfig();
deviceDriver = new SDL2HardwareDeviceDriver();
}
@ -687,7 +703,7 @@ namespace Ryujinx.Ava
Logger.Warning?.Print(LogClass.Audio, "Found OpenAL, changing configuration.");
ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.OpenAl;
MainWindow.SaveConfig();
MainWindowViewModel.SaveConfig();
deviceDriver = new OpenALHardwareDeviceDriver();
}
@ -713,7 +729,7 @@ namespace Ryujinx.Ava
Logger.Warning?.Print(LogClass.Audio, "Found SDL2, changing configuration.");
ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.SDL2;
MainWindow.SaveConfig();
MainWindowViewModel.SaveConfig();
deviceDriver = new SDL2HardwareDeviceDriver();
}
@ -726,7 +742,7 @@ namespace Ryujinx.Ava
Logger.Warning?.Print(LogClass.Audio, "Found SoundIO, changing configuration.");
ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.SoundIo;
MainWindow.SaveConfig();
MainWindowViewModel.SaveConfig();
deviceDriver = new SoundIoHardwareDeviceDriver();
}
@ -743,14 +759,14 @@ namespace Ryujinx.Ava
IntegrityCheckLevel fsIntegrityCheckLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None;
HLE.HLEConfiguration configuration = new HLE.HLEConfiguration(VirtualFileSystem,
_parent.LibHacHorizonManager,
_viewModel.LibHacHorizonManager,
ContentManager,
_accountManager,
_userChannelPersistence,
renderer,
deviceDriver,
memoryConfiguration,
_parent.UiHandler,
_viewModel.UiHandler,
(SystemLanguage)ConfigurationState.Instance.System.Language.Value,
(RegionCode)ConfigurationState.Instance.System.Region.Value,
ConfigurationState.Instance.Graphics.EnableVsync,
@ -791,14 +807,14 @@ namespace Ryujinx.Ava
{
Dispatcher.UIThread.InvokeAsync(() =>
{
if (_parent.ViewModel.StartGamesInFullscreen)
if (_viewModel.StartGamesInFullscreen)
{
_parent.WindowState = WindowState.FullScreen;
_viewModel.WindowState = WindowState.FullScreen;
}
if (_parent.WindowState == WindowState.FullScreen)
if (_viewModel.WindowState == WindowState.FullScreen)
{
_parent.ViewModel.ShowMenuAndStatusBar = false;
_viewModel.ShowMenuAndStatusBar = false;
}
});
@ -822,7 +838,7 @@ namespace Ryujinx.Ava
Width = (int)Renderer.Bounds.Width;
Height = (int)Renderer.Bounds.Height;
_renderer.Window.SetSize((int)(Width * _parent.PlatformImpl.RenderScaling), (int)(Height * _parent.PlatformImpl.RenderScaling));
_renderer.Window.SetSize((int)(Width * _topLevel.PlatformImpl.RenderScaling), (int)(Height * _topLevel.PlatformImpl.RenderScaling));
_chrono.Start();
@ -850,7 +866,7 @@ namespace Ryujinx.Ava
if (!_renderingStarted)
{
_renderingStarted = true;
_parent.SwitchToGameControl();
_viewModel.SwitchToRenderer(false);
}
Device.PresentFrame(() => Renderer?.SwapBuffers());
@ -917,7 +933,7 @@ namespace Ryujinx.Ava
{
Dispatcher.UIThread.Post(() =>
{
_parent.Cursor = _isMouseInRenderer ? InvisibleCursor : Cursor.Default;
_viewModel.Cursor = _isMouseInRenderer ? InvisibleCursor : Cursor.Default;
});
}
else
@ -928,7 +944,7 @@ namespace Ryujinx.Ava
Dispatcher.UIThread.Post(() =>
{
_parent.Cursor = cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency ? InvisibleCursor : Cursor.Default;
_viewModel.Cursor = cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency ? InvisibleCursor : Cursor.Default;
});
}
}
@ -941,22 +957,22 @@ namespace Ryujinx.Ava
return false;
}
if (_parent.IsActive)
if (_viewModel.IsActive)
{
Dispatcher.UIThread.Post(() =>
{
HandleScreenState();
if (_keyboardInterface.GetKeyboardStateSnapshot().IsPressed(Key.Delete) && _parent.WindowState != WindowState.FullScreen)
if (_keyboardInterface.GetKeyboardStateSnapshot().IsPressed(Key.Delete) && _viewModel.WindowState != WindowState.FullScreen)
{
Ptc.Continue();
Device.Application.DiskCacheLoadState?.Cancel();
}
});
}
NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
if (_parent.IsActive)
if (_viewModel.IsActive)
{
KeyboardHotkeyState currentHotkeyState = GetHotkeyState();
@ -972,10 +988,10 @@ namespace Ryujinx.Ava
ScreenshotRequested = true;
break;
case KeyboardHotkeyState.ShowUi:
_parent.ViewModel.ShowMenuAndStatusBar = true;
_viewModel.ShowMenuAndStatusBar = true;
break;
case KeyboardHotkeyState.Pause:
if (_parent.ViewModel.IsPaused)
if (_viewModel.IsPaused)
{
Resume();
}
@ -994,7 +1010,7 @@ namespace Ryujinx.Ava
Device.SetVolume(0);
}
_parent.ViewModel.Volume = Device.GetVolume();
_viewModel.Volume = Device.GetVolume();
break;
case KeyboardHotkeyState.ResScaleUp:
GraphicsConfig.ResScale = GraphicsConfig.ResScale % MaxResolutionScale + 1;
@ -1007,13 +1023,13 @@ namespace Ryujinx.Ava
_newVolume = MathF.Round((Device.GetVolume() + VolumeDelta), 2);
Device.SetVolume(_newVolume);
_parent.ViewModel.Volume = Device.GetVolume();
_viewModel.Volume = Device.GetVolume();
break;
case KeyboardHotkeyState.VolumeDown:
_newVolume = MathF.Round((Device.GetVolume() - VolumeDelta), 2);
Device.SetVolume(_newVolume);
_parent.ViewModel.Volume = Device.GetVolume();
_viewModel.Volume = Device.GetVolume();
break;
case KeyboardHotkeyState.None:
(_keyboardInterface as AvaloniaKeyboard).Clear();
@ -1033,7 +1049,7 @@ namespace Ryujinx.Ava
// Touchscreen
bool hasTouch = false;
if (_parent.IsActive && !ConfigurationState.Instance.Hid.EnableMouse)
if (_viewModel.IsActive && !ConfigurationState.Instance.Hid.EnableMouse)
{
hasTouch = TouchScreenManager.Update(true, (_inputManager.MouseDriver as AvaloniaMouseDriver).IsButtonPressed(MouseButton.Button1), ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
}

View File

@ -269,6 +269,8 @@
<Color x:Key="DataGridSelectionColor">#FF00FABB</Color>
<Color x:Key="ThemeContentBackgroundColor">#FF2D2D2D</Color>
<Color x:Key="ThemeControlBorderColor">#FF505050</Color>
<Color x:Key="VsyncEnabled">#FF2EEAC9</Color>
<Color x:Key="VsyncDisabled">#FFFF4554</Color>
<x:Double x:Key="ScrollBarThickness">15</x:Double>
<x:Double x:Key="FontSizeSmall">8</x:Double>
<x:Double x:Key="FontSizeNormal">10</x:Double>

View File

@ -0,0 +1,111 @@
using Avalonia.Utilities;
using System;
using System.Text;
namespace Ryujinx.Ava.UI.Helper
{
using AvaLogger = Avalonia.Logging.Logger;
using AvaLogLevel = Avalonia.Logging.LogEventLevel;
using RyuLogger = Ryujinx.Common.Logging.Logger;
using RyuLogClass = Ryujinx.Common.Logging.LogClass;
internal class LoggerAdapter : Avalonia.Logging.ILogSink
{
public static void Register()
{
AvaLogger.Sink = new LoggerAdapter();
}
private static RyuLogger.Log? GetLog(AvaLogLevel level)
{
return level switch
{
AvaLogLevel.Verbose => RyuLogger.Trace,
AvaLogLevel.Debug => RyuLogger.Debug,
AvaLogLevel.Information => RyuLogger.Info,
AvaLogLevel.Warning => RyuLogger.Warning,
AvaLogLevel.Error => RyuLogger.Error,
AvaLogLevel.Fatal => RyuLogger.Notice,
_ => throw new ArgumentOutOfRangeException(nameof(level), level, null)
};
}
public bool IsEnabled(AvaLogLevel level, string area)
{
return GetLog(level) != null;
}
public void Log(AvaLogLevel level, string area, object source, string messageTemplate)
{
GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(area, messageTemplate, source, null));
}
public void Log<T0>(AvaLogLevel level, string area, object source, string messageTemplate, T0 propertyValue0)
{
GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(area, messageTemplate, source, new object[] { propertyValue0 }));
}
public void Log<T0, T1>(AvaLogLevel level, string area, object source, string messageTemplate, T0 propertyValue0, T1 propertyValue1)
{
GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(area, messageTemplate, source, new object[] { propertyValue0, propertyValue1 }));
}
public void Log<T0, T1, T2>(AvaLogLevel level, string area, object source, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2)
{
GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(area, messageTemplate, source, new object[] { propertyValue0, propertyValue1, propertyValue2 }));
}
public void Log(AvaLogLevel level, string area, object source, string messageTemplate, params object[] propertyValues)
{
GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(area, messageTemplate, source, propertyValues));
}
private static string Format(string area, string template, object source, object[] v)
{
var result = new StringBuilder();
var r = new CharacterReader(template.AsSpan());
var i = 0;
result.Append('[');
result.Append(area);
result.Append("] ");
while (!r.End)
{
var c = r.Take();
if (c != '{')
{
result.Append(c);
}
else
{
if (r.Peek != '{')
{
result.Append('\'');
result.Append(i < v.Length ? v[i++] : null);
result.Append('\'');
r.TakeUntil('}');
r.Take();
}
else
{
result.Append('{');
r.Take();
}
}
}
if (source != null)
{
result.Append(" (");
result.Append(source.GetType().Name);
result.Append(" #");
result.Append(source.GetHashCode());
result.Append(')');
}
return result.ToString();
}
}
}

View File

@ -11,10 +11,10 @@ namespace Ryujinx.Ava.Input
{
internal class AvaloniaMouseDriver : IGamepadDriver
{
private Control _widget;
private bool _isDisposed;
private Size _size;
private readonly Window _window;
private Control _widget;
private bool _isDisposed;
private Size _size;
private readonly TopLevel _window;
public bool[] PressedButtons { get; }
public Vector2 CurrentPosition { get; private set; }
@ -23,7 +23,7 @@ namespace Ryujinx.Ava.Input
public string DriverName => "AvaloniaMouseDriver";
public ReadOnlySpan<string> GamepadsIds => new[] { "0" };
public AvaloniaMouseDriver(Window window, Control parent)
public AvaloniaMouseDriver(TopLevel window, Control parent)
{
_widget = parent;
_window = window;

View File

@ -56,7 +56,7 @@ namespace Ryujinx.Modules
}
Running = true;
mainWindow.CanUpdate = false;
mainWindow.ViewModel.CanUpdate = false;
// Detect current platform
if (OperatingSystem.IsMacOS())
@ -95,7 +95,7 @@ namespace Ryujinx.Modules
{
using (HttpClient jsonClient = ConstructHttpClient())
{
string buildInfoURL = $"{GitHubApiURL}/repos/{ReleaseInformations.ReleaseChannelOwner}/{ReleaseInformations.ReleaseChannelRepo}/releases/latest";
string buildInfoURL = $"{GitHubApiURL}/repos/{ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}/releases/latest";
string fetchedJson = await jsonClient.GetStringAsync(buildInfoURL);
JObject jsonRoot = JObject.Parse(fetchedJson);
@ -182,7 +182,7 @@ namespace Ryujinx.Modules
}
Running = false;
mainWindow.CanUpdate = true;
mainWindow.ViewModel.CanUpdate = true;
return;
}
@ -625,7 +625,7 @@ namespace Ryujinx.Modules
return false;
}
if (Program.Version.Contains("dirty") || !ReleaseInformations.IsValid())
if (Program.Version.Contains("dirty") || !ReleaseInformation.IsValid())
{
if (showWarnings)
{
@ -640,7 +640,7 @@ namespace Ryujinx.Modules
#else
if (showWarnings)
{
if (ReleaseInformations.IsFlatHubBuild())
if (ReleaseInformation.IsFlatHubBuild())
{
ContentDialogHelper.CreateWarningDialog(LocaleManager.Instance[LocaleKeys.UpdaterDisabledWarningTitle], LocaleManager.Instance[LocaleKeys.DialogUpdaterFlatpakNotSupportedMessage]);
}
@ -711,4 +711,4 @@ namespace Ryujinx.Modules
}
}
}
}
}

View File

@ -1,6 +1,6 @@
using ARMeilleure.Translation.PTC;
using Avalonia;
using Avalonia.Threading;
using Ryujinx.Ava.UI.Helper;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
@ -14,8 +14,10 @@ using Ryujinx.Ui.Common;
using Ryujinx.Ui.Common.Configuration;
using Ryujinx.Ui.Common.Helper;
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Threading.Tasks;
namespace Ryujinx.Ava
@ -33,9 +35,51 @@ namespace Ryujinx.Ava
private const uint MB_ICONWARNING = 0x30;
[SupportedOSPlatform("linux")]
static void RegisterMimeTypes()
{
if (ReleaseInformation.IsFlatHubBuild())
{
return;
}
string mimeDbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share", "mime");
if (!File.Exists(Path.Combine(mimeDbPath, "packages", "Ryujinx.xml")))
{
string mimeTypesFile = Path.Combine(ReleaseInformation.GetBaseApplicationDirectory(), "mime", "Ryujinx.xml");
using Process mimeProcess = new();
mimeProcess.StartInfo.FileName = "xdg-mime";
mimeProcess.StartInfo.Arguments = $"install --novendor --mode user {mimeTypesFile}";
mimeProcess.Start();
mimeProcess.WaitForExit();
if (mimeProcess.ExitCode != 0)
{
Logger.Error?.PrintMsg(LogClass.Application, $"Unable to install mime types. Make sure xdg-utils is installed. Process exited with code: {mimeProcess.ExitCode}");
return;
}
using Process updateMimeProcess = new();
updateMimeProcess.StartInfo.FileName = "update-mime-database";
updateMimeProcess.StartInfo.Arguments = mimeDbPath;
updateMimeProcess.Start();
updateMimeProcess.WaitForExit();
if (updateMimeProcess.ExitCode != 0)
{
Logger.Error?.PrintMsg(LogClass.Application, $"Could not update local mime database. Process exited with code: {updateMimeProcess.ExitCode}");
}
}
}
public static void Main(string[] args)
{
Version = ReleaseInformations.GetVersion();
Version = ReleaseInformation.GetVersion();
if (OperatingSystem.IsWindows() && !OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17134))
{
@ -46,6 +90,8 @@ namespace Ryujinx.Ava
Initialize(args);
LoggerAdapter.Register();
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}
@ -67,8 +113,7 @@ namespace Ryujinx.Ava
AllowEglInitialization = false,
CompositionBackdropCornerRadius = 8.0f,
})
.UseSkia()
.LogToTrace();
.UseSkia();
}
private static void Initialize(string[] args)
@ -94,6 +139,12 @@ namespace Ryujinx.Ava
// Initialize the logger system.
LoggerModule.Initialize();
// Register mime types on linux.
if (OperatingSystem.IsLinux())
{
RegisterMimeTypes();
}
// Initialize Discord integration.
DiscordIntegrationModule.Initialize();
@ -197,9 +248,6 @@ namespace Ryujinx.Ava
private static void ProcessUnhandledException(Exception ex, bool isTerminating)
{
Ptc.Close();
PtcProfiler.Stop();
string message = $"Unhandled exception caught: {ex}";
Logger.Error?.PrintMsg(LogClass.Application, message);
@ -219,10 +267,7 @@ namespace Ryujinx.Ava
{
DiscordIntegrationModule.Exit();
Ptc.Dispose();
PtcProfiler.Dispose();
Logger.Shutdown();
}
}
}
}

View File

@ -76,6 +76,16 @@
</Content>
</ItemGroup>
<ItemGroup Condition="'$(RuntimeIdentifier)' == 'linux-x64'">
<Content Include="..\distribution\linux\Ryujinx.sh">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="..\distribution\linux\mime\Ryujinx.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<TargetPath>mime\Ryujinx.xml</TargetPath>
</Content>
</ItemGroup>
<ItemGroup>
<AvaloniaResource Include="Ui\**\*.xaml">
<SubType>Designer</SubType>

View File

@ -144,9 +144,9 @@ namespace Ryujinx.Ava.UI.Applet
public void ExecuteProgram(Switch device, ProgramSpecifyKind kind, ulong value)
{
device.Configuration.UserChannelPersistence.ExecuteProgram(kind, value);
if (_parent.AppHost != null)
if (_parent.ViewModel.AppHost != null)
{
_parent.AppHost.Stop();
_parent.ViewModel.AppHost.Stop();
}
}

View File

@ -136,7 +136,7 @@ namespace Ryujinx.Ava.UI.Applet
Dispatcher.UIThread.Post(() =>
{
_hiddenTextBox.Clear();
_parent.RendererControl.Focus();
_parent.ViewModel.RendererControl.Focus();
_parent = null;
});

View File

@ -13,8 +13,8 @@ namespace Ryujinx.Ava.UI.Applet
DefaultBackgroundColor = BrushToThemeColor(parent.Background);
DefaultForegroundColor = BrushToThemeColor(parent.Foreground);
DefaultBorderColor = BrushToThemeColor(parent.BorderBrush);
SelectionBackgroundColor = BrushToThemeColor(parent.SearchBox.SelectionBrush);
SelectionForegroundColor = BrushToThemeColor(parent.SearchBox.SelectionForegroundBrush);
SelectionBackgroundColor = BrushToThemeColor(parent.ViewControls.SearchBox.SelectionBrush);
SelectionForegroundColor = BrushToThemeColor(parent.ViewControls.SearchBox.SelectionForegroundBrush);
}
public string FontFamily { get; }

View File

@ -139,6 +139,9 @@
<Style Selector="ListBoxItem:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AppListHoverBackgroundColor}" />
</Style>
<Style Selector="ListBoxItem:selected /template/ Border#SelectionIndicator">
<Setter Property="MinHeight" Value="{Binding $parent[UserControl].DataContext.GridItemSelectorSize}" />
</Style>
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate>

View File

@ -38,9 +38,9 @@ namespace Ryujinx.Ava.UI.Controls
{
if (sender is ListBox listBox)
{
var selected = listBox.SelectedItem as ApplicationData;
_selectedApplication = listBox.SelectedItem as ApplicationData;
_selectedApplication = selected;
(DataContext as MainWindowViewModel).GridSelectedApplication = _selectedApplication;
}
}

View File

@ -137,12 +137,12 @@
<Style Selector="ListBoxItem:selected /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AppListBackgroundColor}" />
</Style>
<Style Selector="ListBoxItem:selected /template/ Border#SelectionIndicator">
<Setter Property="MinHeight" Value="100" />
</Style>
<Style Selector="ListBoxItem:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AppListHoverBackgroundColor}" />
</Style>
<Style Selector="ListBoxItem:selected /template/ Border#SelectionIndicator">
<Setter Property="MinHeight" Value="{Binding $parent[UserControl].DataContext.ListItemSelectorSize}" />
</Style>
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate>

View File

@ -38,9 +38,9 @@ namespace Ryujinx.Ava.UI.Controls
{
if (sender is ListBox listBox)
{
var selected = listBox.SelectedItem as ApplicationData;
_selectedApplication = listBox.SelectedItem as ApplicationData;
_selectedApplication = selected;
(DataContext as MainWindowViewModel).ListSelectedApplication = _selectedApplication;
}
}

View File

@ -19,7 +19,56 @@ namespace Ryujinx.Ava.UI.Helpers
{
private static bool _isChoiceDialogOpen;
private async static Task<UserResult> ShowContentDialog(
public async static Task<UserResult> ShowContentDialog(
string title,
object content,
string primaryButton,
string secondaryButton,
string closeButton,
UserResult primaryButtonResult = UserResult.Ok,
ManualResetEvent deferResetEvent = null,
Func<Window, Task> doWhileDeferred = null,
TypedEventHandler<ContentDialog, ContentDialogButtonClickEventArgs> deferCloseAction = null)
{
UserResult result = UserResult.None;
ContentDialog contentDialog = new()
{
Title = title,
PrimaryButtonText = primaryButton,
SecondaryButtonText = secondaryButton,
CloseButtonText = closeButton,
Content = content
};
contentDialog.PrimaryButtonCommand = MiniCommand.Create(() =>
{
result = primaryButtonResult;
});
contentDialog.SecondaryButtonCommand = MiniCommand.Create(() =>
{
result = UserResult.No;
contentDialog.PrimaryButtonClick -= deferCloseAction;
});
contentDialog.CloseButtonCommand = MiniCommand.Create(() =>
{
result = UserResult.Cancel;
contentDialog.PrimaryButtonClick -= deferCloseAction;
});
if (deferResetEvent != null)
{
contentDialog.PrimaryButtonClick += deferCloseAction;
}
await ShowAsync(contentDialog);
return result;
}
private async static Task<UserResult> ShowTextDialog(
string title,
string primaryText,
string secondaryText,
@ -32,119 +81,9 @@ namespace Ryujinx.Ava.UI.Helpers
Func<Window, Task> doWhileDeferred = null,
TypedEventHandler<ContentDialog, ContentDialogButtonClickEventArgs> deferCloseAction = null)
{
UserResult result = UserResult.None;
Grid content = CreateTextDialogContent(primaryText, secondaryText, iconSymbol);
bool useOverlay = false;
Window mainWindow = null;
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime al)
{
foreach (var item in al.Windows)
{
if (item.IsActive && item is MainWindow window && window.ViewModel.IsGameRunning)
{
mainWindow = window;
useOverlay = true;
break;
}
}
}
ContentDialog contentDialog = null;
ContentDialogOverlayWindow overlay = null;
if (useOverlay)
{
overlay = new ContentDialogOverlayWindow()
{
Height = mainWindow.Bounds.Height,
Width = mainWindow.Bounds.Width,
Position = mainWindow.PointToScreen(new Point())
};
mainWindow.PositionChanged += OverlayOnPositionChanged;
void OverlayOnPositionChanged(object sender, PixelPointEventArgs e)
{
overlay.Position = mainWindow.PointToScreen(new Point());
}
contentDialog = overlay.ContentDialog;
bool opened = false;
overlay.Opened += OverlayOnActivated;
async void OverlayOnActivated(object sender, EventArgs e)
{
if (opened)
{
return;
}
opened = true;
overlay.Position = mainWindow.PointToScreen(new Point());
await ShowDialog();
}
await overlay.ShowDialog(mainWindow);
}
else
{
contentDialog = new ContentDialog();
await ShowDialog();
}
async Task ShowDialog()
{
contentDialog.Title = title;
contentDialog.PrimaryButtonText = primaryButton;
contentDialog.SecondaryButtonText = secondaryButton;
contentDialog.CloseButtonText = closeButton;
contentDialog.Content = CreateDialogTextContent(primaryText, secondaryText, iconSymbol);
contentDialog.PrimaryButtonCommand = MiniCommand.Create(() =>
{
result = primaryButtonResult;
});
contentDialog.SecondaryButtonCommand = MiniCommand.Create(() =>
{
result = UserResult.No;
contentDialog.PrimaryButtonClick -= deferCloseAction;
});
contentDialog.CloseButtonCommand = MiniCommand.Create(() =>
{
result = UserResult.Cancel;
contentDialog.PrimaryButtonClick -= deferCloseAction;
});
if (deferResetEvent != null)
{
contentDialog.PrimaryButtonClick += deferCloseAction;
}
if (useOverlay)
{
await contentDialog.ShowAsync(overlay, ContentDialogPlacement.Popup);
overlay!.Close();
}
else
{
await contentDialog.ShowAsync(ContentDialogPlacement.Popup);
}
}
if (useOverlay)
{
overlay.Content = null;
overlay.Close();
}
return result;
return await ShowContentDialog(title, content, primaryButton, secondaryButton, closeButton, primaryButtonResult, deferResetEvent, doWhileDeferred, deferCloseAction);
}
public async static Task<UserResult> ShowDeferredContentDialog(
@ -162,7 +101,7 @@ namespace Ryujinx.Ava.UI.Helpers
bool startedDeferring = false;
UserResult result = UserResult.None;
return await ShowContentDialog(
return await ShowTextDialog(
title,
primaryText,
secondaryText,
@ -192,8 +131,7 @@ namespace Ryujinx.Ava.UI.Helpers
sender.PrimaryButtonClick -= DeferClose;
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
Task.Run(() =>
_ = Task.Run(() =>
{
deferResetEvent.WaitOne();
@ -202,7 +140,6 @@ namespace Ryujinx.Ava.UI.Helpers
deferral.Complete();
});
});
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
if (doWhileDeferred != null)
{
@ -213,34 +150,42 @@ namespace Ryujinx.Ava.UI.Helpers
}
}
private static Grid CreateDialogTextContent(string primaryText, string secondaryText, int symbol)
private static Grid CreateTextDialogContent(string primaryText, string secondaryText, int symbol)
{
Grid content = new Grid();
content.RowDefinitions = new RowDefinitions() { new RowDefinition(), new RowDefinition() };
content.ColumnDefinitions = new ColumnDefinitions() { new ColumnDefinition(GridLength.Auto), new ColumnDefinition() };
Grid content = new()
{
RowDefinitions = new RowDefinitions() { new RowDefinition(), new RowDefinition() },
ColumnDefinitions = new ColumnDefinitions() { new ColumnDefinition(GridLength.Auto), new ColumnDefinition() },
content.MinHeight = 80;
MinHeight = 80
};
SymbolIcon icon = new()
{
Symbol = (Symbol)symbol,
Margin = new Thickness(10),
FontSize = 40,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
};
SymbolIcon icon = new SymbolIcon { Symbol = (Symbol)symbol, Margin = new Thickness(10) };
icon.FontSize = 40;
icon.VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center;
Grid.SetColumn(icon, 0);
Grid.SetRowSpan(icon, 2);
Grid.SetRow(icon, 0);
TextBlock primaryLabel = new TextBlock()
TextBlock primaryLabel = new()
{
Text = primaryText,
Margin = new Thickness(5),
Text = primaryText,
Margin = new Thickness(5),
TextWrapping = TextWrapping.Wrap,
MaxWidth = 450
MaxWidth = 450
};
TextBlock secondaryLabel = new TextBlock()
TextBlock secondaryLabel = new()
{
Text = secondaryText,
Margin = new Thickness(5),
Text = secondaryText,
Margin = new Thickness(5),
TextWrapping = TextWrapping.Wrap,
MaxWidth = 450
MaxWidth = 450
};
Grid.SetColumn(primaryLabel, 1);
@ -262,7 +207,7 @@ namespace Ryujinx.Ava.UI.Helpers
string closeButton,
string title)
{
return await ShowContentDialog(
return await ShowTextDialog(
title,
primary,
secondaryText,
@ -280,7 +225,7 @@ namespace Ryujinx.Ava.UI.Helpers
string title,
UserResult primaryButtonResult = UserResult.Yes)
{
return await ShowContentDialog(
return await ShowTextDialog(
string.IsNullOrWhiteSpace(title) ? LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle] : title,
primaryText,
secondaryText,
@ -298,7 +243,7 @@ namespace Ryujinx.Ava.UI.Helpers
internal static async Task CreateUpdaterInfoDialog(string primary, string secondaryText)
{
await ShowContentDialog(
await ShowTextDialog(
LocaleManager.Instance[LocaleKeys.DialogUpdaterTitle],
primary,
secondaryText,
@ -310,7 +255,7 @@ namespace Ryujinx.Ava.UI.Helpers
internal static async Task CreateWarningDialog(string primary, string secondaryText)
{
await ShowContentDialog(
await ShowTextDialog(
LocaleManager.Instance[LocaleKeys.DialogWarningTitle],
primary,
secondaryText,
@ -324,7 +269,7 @@ namespace Ryujinx.Ava.UI.Helpers
{
Logger.Error?.Print(LogClass.Application, errorMessage);
await ShowContentDialog(
await ShowTextDialog(
LocaleManager.Instance[LocaleKeys.DialogErrorTitle],
LocaleManager.Instance[LocaleKeys.DialogErrorMessage],
errorMessage,
@ -343,16 +288,15 @@ namespace Ryujinx.Ava.UI.Helpers
_isChoiceDialogOpen = true;
UserResult response =
await ShowContentDialog(
title,
primary,
secondaryText,
LocaleManager.Instance[LocaleKeys.InputDialogYes],
"",
LocaleManager.Instance[LocaleKeys.InputDialogNo],
(int)Symbol.Help,
UserResult.Yes);
UserResult response = await ShowTextDialog(
title,
primary,
secondaryText,
LocaleManager.Instance[LocaleKeys.InputDialogYes],
"",
LocaleManager.Instance[LocaleKeys.InputDialogNo],
(int)Symbol.Help,
UserResult.Yes);
_isChoiceDialogOpen = false;
@ -396,5 +340,98 @@ namespace Ryujinx.Ava.UI.Helpers
return string.Empty;
}
public static async Task<ContentDialogResult> ShowAsync(ContentDialog contentDialog)
{
ContentDialogResult result;
ContentDialogOverlayWindow contentDialogOverlayWindow = null;
Window parent = GetMainWindow();
if (parent.IsActive && parent is MainWindow window && window.ViewModel.IsGameRunning)
{
contentDialogOverlayWindow = new()
{
Height = parent.Bounds.Height,
Width = parent.Bounds.Width,
Position = parent.PointToScreen(new Point()),
ShowInTaskbar = false
};
parent.PositionChanged += OverlayOnPositionChanged;
void OverlayOnPositionChanged(object sender, PixelPointEventArgs e)
{
contentDialogOverlayWindow.Position = parent.PointToScreen(new Point());
}
contentDialogOverlayWindow.ContentDialog = contentDialog;
bool opened = false;
contentDialogOverlayWindow.Opened += OverlayOnActivated;
async void OverlayOnActivated(object sender, EventArgs e)
{
if (opened)
{
return;
}
opened = true;
contentDialogOverlayWindow.Position = parent.PointToScreen(new Point());
result = await ShowDialog();
}
result = await contentDialogOverlayWindow.ShowDialog<ContentDialogResult>(parent);
}
else
{
result = await ShowDialog();
}
async Task<ContentDialogResult> ShowDialog()
{
if (contentDialogOverlayWindow is not null)
{
result = await contentDialog.ShowAsync(contentDialogOverlayWindow);
contentDialogOverlayWindow!.Close();
}
else
{
result = await contentDialog.ShowAsync();
}
return result;
}
if (contentDialogOverlayWindow is not null)
{
contentDialogOverlayWindow.Content = null;
contentDialogOverlayWindow.Close();
}
return result;
}
private static Window GetMainWindow()
{
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime al)
{
foreach (Window item in al.Windows)
{
if (item.IsActive && item is MainWindow window)
{
return window;
}
}
}
return null;
}
}
}

View File

@ -0,0 +1,136 @@
using Avalonia;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Common.Utilities;
using Ryujinx.Ui.Common.Configuration;
using System;
using System.Net.Http;
using System.Net.NetworkInformation;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.ViewModels
{
public class AboutWindowViewModel : BaseModel
{
private Bitmap _githubLogo;
private Bitmap _discordLogo;
private Bitmap _patreonLogo;
private Bitmap _twitterLogo;
private string _version;
private string _supporters;
public Bitmap GithubLogo
{
get => _githubLogo;
set
{
_githubLogo = value;
OnPropertyChanged();
}
}
public Bitmap DiscordLogo
{
get => _discordLogo;
set
{
_discordLogo = value;
OnPropertyChanged();
}
}
public Bitmap PatreonLogo
{
get => _patreonLogo;
set
{
_patreonLogo = value;
OnPropertyChanged();
}
}
public Bitmap TwitterLogo
{
get => _twitterLogo;
set
{
_twitterLogo = value;
OnPropertyChanged();
}
}
public string Supporters
{
get => _supporters;
set
{
_supporters = value;
OnPropertyChanged();
}
}
public string Version
{
get => _version;
set
{
_version = value;
OnPropertyChanged();
}
}
public string Developers
{
get => string.Format(LocaleManager.Instance[LocaleKeys.AboutPageDeveloperListMore], "gdkchan, Ac_K, marysaka, rip in peri peri, LDj3SNuD, emmaus, Thealexbarney, GoffyDude, TSRBerry, IsaacMarovitz");
}
public AboutWindowViewModel()
{
Version = Program.Version;
var assets = AvaloniaLocator.Current.GetService<Avalonia.Platform.IAssetLoader>();
if (ConfigurationState.Instance.Ui.BaseStyle.Value == "Light")
{
GithubLogo = new Bitmap(assets.Open(new Uri("resm:Ryujinx.Ui.Common.Resources.Logo_GitHub_Light.png?assembly=Ryujinx.Ui.Common")));
DiscordLogo = new Bitmap(assets.Open(new Uri("resm:Ryujinx.Ui.Common.Resources.Logo_Discord_Light.png?assembly=Ryujinx.Ui.Common")));
PatreonLogo = new Bitmap(assets.Open(new Uri("resm:Ryujinx.Ui.Common.Resources.Logo_Patreon_Light.png?assembly=Ryujinx.Ui.Common")));
TwitterLogo = new Bitmap(assets.Open(new Uri("resm:Ryujinx.Ui.Common.Resources.Logo_Twitter_Light.png?assembly=Ryujinx.Ui.Common")));
}
else
{
GithubLogo = new Bitmap(assets.Open(new Uri("resm:Ryujinx.Ui.Common.Resources.Logo_GitHub_Dark.png?assembly=Ryujinx.Ui.Common")));
DiscordLogo = new Bitmap(assets.Open(new Uri("resm:Ryujinx.Ui.Common.Resources.Logo_Discord_Dark.png?assembly=Ryujinx.Ui.Common")));
PatreonLogo = new Bitmap(assets.Open(new Uri("resm:Ryujinx.Ui.Common.Resources.Logo_Patreon_Dark.png?assembly=Ryujinx.Ui.Common")));
TwitterLogo = new Bitmap(assets.Open(new Uri("resm:Ryujinx.Ui.Common.Resources.Logo_Twitter_Dark.png?assembly=Ryujinx.Ui.Common")));
}
Dispatcher.UIThread.InvokeAsync(DownloadPatronsJson);
}
private async Task DownloadPatronsJson()
{
if (!NetworkInterface.GetIsNetworkAvailable())
{
Supporters = LocaleManager.Instance[LocaleKeys.ConnectionError];
return;
}
HttpClient httpClient = new();
try
{
string patreonJsonString = await httpClient.GetStringAsync("https://patreon.ryujinx.org/");
Supporters = string.Join(", ", JsonHelper.Deserialize<string[]>(patreonJsonString)) + "\n\n";
}
catch
{
Supporters = LocaleManager.Instance[LocaleKeys.ApiError];
}
}
}
}

View File

@ -244,9 +244,9 @@ namespace Ryujinx.Ava.UI.ViewModels
_mainWindow.InputManager.GamepadDriver.OnGamepadConnected += HandleOnGamepadConnected;
_mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected;
if (_mainWindow.AppHost != null)
if (_mainWindow.ViewModel.AppHost != null)
{
_mainWindow.AppHost.NpadManager.BlockInputUpdates();
_mainWindow.ViewModel.AppHost.NpadManager.BlockInputUpdates();
}
_isLoaded = false;
@ -862,7 +862,7 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
_mainWindow.AppHost?.NpadManager.ReloadConfiguration(newConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse);
_mainWindow.ViewModel.AppHost?.NpadManager.ReloadConfiguration(newConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse);
// Atomically replace and signal input change.
// NOTE: Do not modify InputConfig.Value directly as other code depends on the on-change event.
@ -891,7 +891,7 @@ namespace Ryujinx.Ava.UI.ViewModels
_mainWindow.InputManager.GamepadDriver.OnGamepadConnected -= HandleOnGamepadConnected;
_mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected -= HandleOnGamepadDisconnected;
_mainWindow.AppHost?.NpadManager.UnblockInputUpdates();
_mainWindow.ViewModel.AppHost?.NpadManager.UnblockInputUpdates();
SelectedGamepad?.Dispose();

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,3 @@
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Threading;
@ -8,8 +7,6 @@ using Ryujinx.Audio.Backends.OpenAL;
using Ryujinx.Audio.Backends.SDL2;
using Ryujinx.Audio.Backends.SoundIo;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Input;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common.Configuration;
@ -19,7 +16,6 @@ using Ryujinx.Common.Logging;
using Ryujinx.Graphics.Vulkan;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS.Services.Time.TimeZone;
using Ryujinx.Input;
using Ryujinx.Ui.Common.Configuration;
using Ryujinx.Ui.Common.Configuration.System;
using System;
@ -30,11 +26,10 @@ using TimeZone = Ryujinx.Ava.UI.Models.TimeZone;
namespace Ryujinx.Ava.UI.ViewModels
{
internal class SettingsViewModel : BaseModel
public class SettingsViewModel : BaseModel
{
private readonly VirtualFileSystem _virtualFileSystem;
private readonly ContentManager _contentManager;
private readonly StyleableWindow _owner;
private TimeZoneContentManager _timeZoneContentManager;
private readonly List<string> _validTzRegions;
@ -44,10 +39,14 @@ namespace Ryujinx.Ava.UI.ViewModels
private int _graphicsBackendMultithreadingIndex;
private float _volume;
private bool _isVulkanAvailable = true;
private bool _directoryChanged = false;
private List<string> _gpuIds = new List<string>();
private bool _directoryChanged;
private List<string> _gpuIds = new();
private KeyboardHotkeys _keyboardHotkeys;
private int _graphicsBackendIndex;
private string _customThemePath;
public event Action CloseWindow;
public event Action SaveSettingsEvent;
public int ResolutionScale
{
@ -67,19 +66,16 @@ namespace Ryujinx.Ava.UI.ViewModels
{
_graphicsBackendMultithreadingIndex = value;
if (_owner != null)
if (_graphicsBackendMultithreadingIndex != (int)ConfigurationState.Instance.Graphics.BackendThreading.Value)
{
if (_graphicsBackendMultithreadingIndex != (int)ConfigurationState.Instance.Graphics.BackendThreading.Value)
Dispatcher.UIThread.Post(async () =>
{
Dispatcher.UIThread.Post(async () =>
{
await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogSettingsBackendThreadingWarningMessage],
"",
"",
LocaleManager.Instance[LocaleKeys.InputDialogOk],
LocaleManager.Instance[LocaleKeys.DialogSettingsBackendThreadingWarningTitle]);
});
}
await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogSettingsBackendThreadingWarningMessage],
"",
"",
LocaleManager.Instance[LocaleKeys.InputDialogOk],
LocaleManager.Instance[LocaleKeys.DialogSettingsBackendThreadingWarningTitle]);
});
}
OnPropertyChanged();
@ -120,12 +116,12 @@ namespace Ryujinx.Ava.UI.ViewModels
OnPropertyChanged();
}
}
public bool IsMacOS
{
get => OperatingSystem.IsMacOS();
}
public bool EnableDiscordIntegration { get; set; }
public bool CheckUpdatesOnStart { get; set; }
public bool ShowConfirmExit { get; set; }
@ -160,7 +156,20 @@ namespace Ryujinx.Ava.UI.ViewModels
public string TimeZone { get; set; }
public string ShaderDumpPath { get; set; }
public string CustomThemePath { get; set; }
public string CustomThemePath
{
get
{
return _customThemePath;
}
set
{
_customThemePath = value;
OnPropertyChanged();
}
}
public int Language { get; set; }
public int Region { get; set; }
@ -191,7 +200,7 @@ namespace Ryujinx.Ava.UI.ViewModels
{
_volume = value;
ConfigurationState.Instance.System.AudioVolume.Value = (float)(_volume / 100);
ConfigurationState.Instance.System.AudioVolume.Value = _volume / 100;
OnPropertyChanged();
}
@ -199,7 +208,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public DateTimeOffset DateOffset { get; set; }
public TimeSpan TimeOffset { get; set; }
public AvaloniaList<TimeZone> TimeZones { get; set; }
private AvaloniaList<TimeZone> TimeZones { get; set; }
public AvaloniaList<string> GameDirectories { get; set; }
public ObservableCollection<ComboBoxItem> AvailableGpus { get; set; }
@ -214,17 +223,13 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
public IGamepadDriver AvaloniaKeyboardDriver { get; }
public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager, StyleableWindow owner) : this()
public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this()
{
_virtualFileSystem = virtualFileSystem;
_contentManager = contentManager;
_owner = owner;
if (Program.PreviewerDetached)
{
LoadTimeZones();
AvaloniaKeyboardDriver = new AvaloniaKeyboardDriver(owner);
}
}
@ -251,10 +256,10 @@ namespace Ryujinx.Ava.UI.ViewModels
IsSDL2Enabled = SDL2HardwareDeviceDriver.IsSupported;
}
private unsafe void LoadAvailableGpus()
private void LoadAvailableGpus()
{
_gpuIds = new List<string>();
List<string> names = new List<string>();
List<string> names = new();
var devices = VulkanRenderer.GetPhysicalDevices();
if (devices.Length == 0)
@ -272,7 +277,7 @@ namespace Ryujinx.Ava.UI.ViewModels
}
AvailableGpus.Clear();
AvailableGpus.AddRange(names.Select(x => new ComboBoxItem() { Content = x }));
AvailableGpus.AddRange(names.Select(x => new ComboBoxItem { Content = x }));
}
public void LoadTimeZones()
@ -302,25 +307,6 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
public async void BrowseTheme()
{
var dialog = new OpenFileDialog()
{
Title = LocaleManager.Instance[LocaleKeys.SettingsSelectThemeFileDialogTitle],
AllowMultiple = false
};
dialog.Filters.Add(new FileDialogFilter() { Extensions = { "xaml" }, Name = LocaleManager.Instance[LocaleKeys.SettingsXamlThemeFile] });
var file = await dialog.ShowAsync(_owner);
if (file != null && file.Length > 0)
{
CustomThemePath = file[0];
OnPropertyChanged(nameof(CustomThemePath));
}
}
public void LoadCurrentConfiguration()
{
ConfigurationState config = ConfigurationState.Instance;
@ -477,16 +463,8 @@ namespace Ryujinx.Ava.UI.ViewModels
config.ToFileFormat().SaveConfig(Program.ConfigurationPath);
MainWindow.UpdateGraphicsConfig();
if (_owner is SettingsWindow owner)
{
owner.ControllerSettings?.SaveCurrentProfile();
}
if (_owner.Owner is MainWindow window && _directoryChanged)
{
window.ViewModel.LoadApplications();
}
SaveSettingsEvent?.Invoke();
_directoryChanged = false;
}
@ -504,13 +482,13 @@ namespace Ryujinx.Ava.UI.ViewModels
public void OkButton()
{
SaveSettings();
_owner.Close();
CloseWindow?.Invoke();
}
public void CancelButton()
{
RevertIfNotSaved();
_owner.Close();
CloseWindow?.Invoke();
}
}
}

View File

@ -0,0 +1,216 @@
<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:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
mc:Ignorable="d"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
x:DataType="viewModels:MainWindowViewModel"
x:Class="Ryujinx.Ava.UI.Views.Main.MainMenuBarView"
x:CompileBindings="True">
<Design.DataContext>
<viewModels:MainWindowViewModel />
</Design.DataContext>
<DockPanel HorizontalAlignment="Stretch">
<Menu
Name="Menu"
Height="35"
Margin="0"
HorizontalAlignment="Left">
<Menu.ItemsPanel>
<ItemsPanelTemplate>
<DockPanel Margin="0" HorizontalAlignment="Stretch" />
</ItemsPanelTemplate>
</Menu.ItemsPanel>
<MenuItem VerticalAlignment="Center" Header="{locale:Locale MenuBarFile}">
<MenuItem
Command="{ReflectionBinding OpenFile}"
Header="{locale:Locale MenuBarFileOpenFromFile}"
IsEnabled="{Binding EnableNonGameRunningControls}"
ToolTip.Tip="{locale:Locale LoadApplicationFileTooltip}" />
<MenuItem
Command="{ReflectionBinding OpenFolder}"
Header="{locale:Locale MenuBarFileOpenUnpacked}"
IsEnabled="{Binding EnableNonGameRunningControls}"
ToolTip.Tip="{locale:Locale LoadApplicationFolderTooltip}" />
<MenuItem Header="{locale:Locale MenuBarFileOpenApplet}" IsEnabled="{Binding IsAppletMenuActive}">
<MenuItem
Click="OpenMiiApplet"
Header="Mii Edit Applet"
ToolTip.Tip="{locale:Locale MenuBarFileOpenAppletOpenMiiAppletToolTip}" />
</MenuItem>
<Separator />
<MenuItem
Command="{ReflectionBinding OpenRyujinxFolder}"
Header="{locale:Locale MenuBarFileOpenEmuFolder}"
ToolTip.Tip="{locale:Locale OpenRyujinxFolderTooltip}" />
<MenuItem
Command="{ReflectionBinding OpenLogsFolder}"
Header="{locale:Locale MenuBarFileOpenLogsFolder}"
ToolTip.Tip="{locale:Locale OpenRyujinxLogsTooltip}" />
<Separator />
<MenuItem
Click="CloseWindow"
Header="{locale:Locale MenuBarFileExit}"
ToolTip.Tip="{locale:Locale ExitTooltip}" />
</MenuItem>
<MenuItem VerticalAlignment="Center" Header="{locale:Locale MenuBarOptions}">
<MenuItem
Command="{ReflectionBinding ToggleFullscreen}"
Header="{locale:Locale MenuBarOptionsToggleFullscreen}"
InputGesture="F11" />
<MenuItem>
<MenuItem.Icon>
<CheckBox IsChecked="{Binding StartGamesInFullscreen, Mode=TwoWay}"
MinWidth="250">
<TextBlock Text="{locale:Locale MenuBarOptionsStartGamesInFullscreen}"/>
</CheckBox>
</MenuItem.Icon>
</MenuItem>
<MenuItem IsVisible="{Binding ShowConsoleVisible}">
<MenuItem.Icon>
<CheckBox IsChecked="{Binding ShowConsole, Mode=TwoWay}"
MinWidth="250">
<TextBlock Text="{locale:Locale MenuBarOptionsShowConsole}"/>
</CheckBox>
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="{locale:Locale MenuBarOptionsChangeLanguage}">
<MenuItem
Command="{ReflectionBinding ChangeLanguage}"
CommandParameter="de_DE"
Header="Deutsch" />
<MenuItem
Command="{ReflectionBinding ChangeLanguage}"
CommandParameter="en_US"
Header="English (US)" />
<MenuItem
Command="{ReflectionBinding ChangeLanguage}"
CommandParameter="es_ES"
Header="Español (ES)" />
<MenuItem
Command="{ReflectionBinding ChangeLanguage}"
CommandParameter="fr_FR"
Header="Français" />
<MenuItem
Command="{ReflectionBinding ChangeLanguage}"
CommandParameter="it_IT"
Header="Italiano" />
<MenuItem
Command="{ReflectionBinding ChangeLanguage}"
CommandParameter="pt_BR"
Header="Português (BR)" />
<MenuItem
Command="{ReflectionBinding ChangeLanguage}"
CommandParameter="tr_TR"
Header="Türkçe" />
<MenuItem
Command="{ReflectionBinding ChangeLanguage}"
CommandParameter="el_GR"
Header="Ελληνικά" />
<MenuItem
Command="{ReflectionBinding ChangeLanguage}"
CommandParameter="pl_PL"
Header="Polski" />
<MenuItem
Command="{ReflectionBinding ChangeLanguage}"
CommandParameter="ru_RU"
Header="Русский" />
<MenuItem
Command="{ReflectionBinding ChangeLanguage}"
CommandParameter="zh_CN"
Header="简体中文" />
<MenuItem
Command="{ReflectionBinding ChangeLanguage}"
CommandParameter="zh_TW"
Header="繁體中文" />
<MenuItem
Command="{ReflectionBinding ChangeLanguage}"
CommandParameter="ja_JP"
Header="日本語" />
<MenuItem
Command="{ReflectionBinding ChangeLanguage}"
CommandParameter="ko_KR"
Header="한국어" />
</MenuItem>
<Separator />
<MenuItem
Click="OpenSettings"
Header="{locale:Locale MenuBarOptionsSettings}"
ToolTip.Tip="{locale:Locale OpenSettingsTooltip}" />
<MenuItem
Command="{ReflectionBinding ManageProfiles}"
Header="{locale:Locale MenuBarOptionsManageUserProfiles}"
IsEnabled="{Binding EnableNonGameRunningControls}"
ToolTip.Tip="{locale:Locale OpenProfileManagerTooltip}" />
</MenuItem>
<MenuItem
Name="ActionsMenuItem"
VerticalAlignment="Center"
Header="{locale:Locale MenuBarActions}"
IsEnabled="{Binding IsGameRunning}">
<MenuItem
Click="PauseEmulation_Click"
Header="{locale:Locale MenuBarOptionsPauseEmulation}"
InputGesture="{Binding PauseKey}"
IsEnabled="{Binding !IsPaused}"
IsVisible="{Binding !IsPaused}" />
<MenuItem
Click="ResumeEmulation_Click"
Header="{locale:Locale MenuBarOptionsResumeEmulation}"
InputGesture="{Binding PauseKey}"
IsEnabled="{Binding IsPaused}"
IsVisible="{Binding IsPaused}" />
<MenuItem
Click="StopEmulation_Click"
Header="{locale:Locale MenuBarOptionsStopEmulation}"
InputGesture="Escape"
IsEnabled="{Binding IsGameRunning}"
ToolTip.Tip="{locale:Locale StopEmulationTooltip}" />
<MenuItem Command="{ReflectionBinding SimulateWakeUpMessage}" Header="{locale:Locale MenuBarOptionsSimulateWakeUpMessage}" />
<Separator />
<MenuItem
Name="ScanAmiiboMenuItem"
AttachedToVisualTree="ScanAmiiboMenuItem_AttachedToVisualTree"
Click="OpenAmiiboWindow"
Header="{locale:Locale MenuBarActionsScanAmiibo}"
IsEnabled="{Binding IsAmiiboRequested}" />
<MenuItem
Command="{ReflectionBinding TakeScreenshot}"
Header="{locale:Locale MenuBarFileToolsTakeScreenshot}"
InputGesture="{Binding ScreenshotKey}"
IsEnabled="{Binding IsGameRunning}" />
<MenuItem
Command="{ReflectionBinding HideUi}"
Header="{locale:Locale MenuBarFileToolsHideUi}"
InputGesture="{Binding ShowUiKey}"
IsEnabled="{Binding IsGameRunning}" />
<MenuItem
Click="OpenCheatManagerForCurrentApp"
Header="{locale:Locale GameListContextMenuManageCheat}"
IsEnabled="{Binding IsGameRunning}" />
</MenuItem>
<MenuItem VerticalAlignment="Center" Header="{locale:Locale MenuBarTools}">
<MenuItem Header="{locale:Locale MenuBarToolsInstallFirmware}" IsEnabled="{Binding EnableNonGameRunningControls}">
<MenuItem Command="{ReflectionBinding InstallFirmwareFromFile}" Header="{locale:Locale MenuBarFileToolsInstallFirmwareFromFile}" />
<MenuItem Command="{ReflectionBinding InstallFirmwareFromFolder}" Header="{locale:Locale MenuBarFileToolsInstallFirmwareFromDirectory}" />
</MenuItem>
</MenuItem>
<MenuItem VerticalAlignment="Center" Header="{locale:Locale MenuBarHelp}">
<MenuItem
Name="UpdateMenuItem"
IsEnabled="{Binding CanUpdate}"
Click="CheckForUpdates"
Header="{locale:Locale MenuBarHelpCheckForUpdates}"
ToolTip.Tip="{locale:Locale CheckUpdatesTooltip}" />
<Separator />
<MenuItem
Click="OpenAboutWindow"
Header="{locale:Locale MenuBarHelpAbout}"
ToolTip.Tip="{locale:Locale OpenAboutTooltip}" />
</MenuItem>
</Menu>
</DockPanel>
</UserControl>

View File

@ -0,0 +1,143 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows;
using System.Threading.Tasks;
using LibHac.FsSystem;
using LibHac.Ncm;
using Ryujinx.HLE.HOS;
using Ryujinx.Modules;
namespace Ryujinx.Ava.UI.Views.Main
{
public partial class MainMenuBarView : UserControl
{
public MainWindow Window { get; private set; }
public MainWindowViewModel ViewModel { get; private set; }
public MainMenuBarView()
{
InitializeComponent();
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (this.VisualRoot is MainWindow window)
{
Window = window;
}
ViewModel = Window.ViewModel;
DataContext = ViewModel;
}
private async void StopEmulation_Click(object sender, RoutedEventArgs e)
{
await Window.ViewModel.AppHost?.ShowExitPrompt();
}
private async void PauseEmulation_Click(object sender, RoutedEventArgs e)
{
await Task.Run(() =>
{
Window.ViewModel.AppHost?.Pause();
});
}
private async void ResumeEmulation_Click(object sender, RoutedEventArgs e)
{
await Task.Run(() =>
{
Window.ViewModel.AppHost?.Resume();
});
}
public async void OpenSettings(object sender, RoutedEventArgs e)
{
Window.SettingsWindow = new(Window.VirtualFileSystem, Window.ContentManager);
await Window.SettingsWindow.ShowDialog(Window);
ViewModel.LoadConfigurableHotKeys();
}
public void OpenMiiApplet(object sender, RoutedEventArgs e)
{
string contentPath = ViewModel.ContentManager.GetInstalledContentPath(0x0100000000001009, StorageId.BuiltInSystem, NcaContentType.Program);
if (!string.IsNullOrEmpty(contentPath))
{
ViewModel.LoadApplication(contentPath, false, "Mii Applet");
}
}
public async void OpenAmiiboWindow(object sender, RoutedEventArgs e)
{
if (!ViewModel.IsAmiiboRequested)
{
return;
}
if (ViewModel.AppHost.Device.System.SearchingForAmiibo(out int deviceId))
{
string titleId = ViewModel.AppHost.Device.Application.TitleIdText.ToUpper();
AmiiboWindow window = new(ViewModel.ShowAll, ViewModel.LastScannedAmiiboId, titleId);
await window.ShowDialog(Window);
if (window.IsScanned)
{
ViewModel.ShowAll = window.ViewModel.ShowAllAmiibo;
ViewModel.LastScannedAmiiboId = window.ScannedAmiibo.GetId();
ViewModel.AppHost.Device.System.ScanAmiibo(deviceId, ViewModel.LastScannedAmiiboId, window.ViewModel.UseRandomUuid);
}
}
}
public async void OpenCheatManagerForCurrentApp(object sender, RoutedEventArgs e)
{
if (!ViewModel.IsGameRunning)
{
return;
}
ApplicationLoader application = ViewModel.AppHost.Device.Application;
if (application != null)
{
await new CheatWindow(Window.VirtualFileSystem, application.TitleIdText, application.TitleName).ShowDialog(Window);
ViewModel.AppHost.Device.EnableCheats();
}
}
private void ScanAmiiboMenuItem_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
{
if (sender is MenuItem)
{
ViewModel.IsAmiiboRequested = Window.ViewModel.AppHost.Device.System.SearchingForAmiibo(out _);
}
}
public async void CheckForUpdates(object sender, RoutedEventArgs e)
{
if (Updater.CanUpdate(true, Window))
{
await Updater.BeginParse(Window, true);
}
}
public async void OpenAboutWindow(object sender, RoutedEventArgs e)
{
await AboutWindow.Show();
}
public void CloseWindow(object sender, RoutedEventArgs e)
{
Window.Close();
}
}
}

View File

@ -0,0 +1,232 @@
<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:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ryujinx.Ava.UI.Views.Main.MainStatusBarView"
x:CompileBindings="True"
x:DataType="viewModels:MainWindowViewModel">
<Design.DataContext>
<viewModels:MainWindowViewModel />
</Design.DataContext>
<Grid
Name="StatusBar"
Margin="0"
MinHeight="22"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Background="{DynamicResource ThemeContentBackgroundColor}"
DockPanel.Dock="Bottom"
IsVisible="{Binding ShowMenuAndStatusBar}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel
Grid.Column="0"
Margin="5"
VerticalAlignment="Center"
IsVisible="{Binding EnableNonGameRunningControls}">
<Grid Margin="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Button
Width="25"
Height="25"
MinWidth="0"
Margin="0,0,5,0"
VerticalAlignment="Center"
Background="Transparent"
Command="{ReflectionBinding LoadApplications}">
<ui:SymbolIcon
Width="50"
Height="100"
Symbol="Refresh" />
</Button>
<TextBlock
Name="LoadStatus"
Grid.Column="1"
Margin="0,0,5,0"
VerticalAlignment="Center"
IsVisible="{Binding EnableNonGameRunningControls}"
Text="{locale:Locale StatusBarGamesLoaded}" />
<ProgressBar
Name="LoadProgressBar"
Grid.Column="2"
Height="6"
VerticalAlignment="Center"
Foreground="{DynamicResource HighlightColor}"
IsVisible="{Binding StatusBarVisible}"
Maximum="{Binding StatusBarProgressMaximum}"
Value="{Binding StatusBarProgressValue}" />
</Grid>
</StackPanel>
<StackPanel
Grid.Column="1"
Margin="0,2"
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsVisible="{Binding IsGameRunning}"
MaxHeight="18"
Orientation="Horizontal">
<TextBlock
Name="VsyncStatus"
Margin="5,0,5,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Foreground="{Binding VsyncColor}"
IsVisible="{Binding !ShowLoadProgress}"
PointerReleased="VsyncStatus_PointerReleased"
Text="VSync"
TextAlignment="Left" />
<Border
Width="2"
Height="12"
Margin="0"
BorderBrush="Gray"
BorderThickness="1"
IsVisible="{Binding !ShowLoadProgress}" />
<TextBlock
Name="DockedStatus"
Margin="5,0,5,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsVisible="{Binding !ShowLoadProgress}"
PointerReleased="DockedStatus_PointerReleased"
Text="{Binding DockedStatusText}"
TextAlignment="Left" />
<Border
Width="2"
Height="12"
Margin="0"
BorderBrush="Gray"
BorderThickness="1"
IsVisible="{Binding !ShowLoadProgress}" />
<TextBlock
Name="AspectRatioStatus"
Margin="5,0,5,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsVisible="{Binding !ShowLoadProgress}"
PointerReleased="AspectRatioStatus_PointerReleased"
Text="{Binding AspectRatioStatusText}"
TextAlignment="Left" />
<Border
Width="2"
Height="12"
Margin="0"
BorderBrush="Gray"
BorderThickness="1"
IsVisible="{Binding !ShowLoadProgress}" />
<ui:ToggleSplitButton
Name="VolumeStatus"
Padding="5,0,5,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
VerticalContentAlignment="Center"
Background="{DynamicResource ThemeContentBackgroundColor}"
BorderThickness="0"
Content="{Binding VolumeStatusText}"
IsChecked="{Binding VolumeMuted}"
IsVisible="{Binding !ShowLoadProgress}">
<ui:ToggleSplitButton.Flyout>
<Flyout Placement="Bottom" ShowMode="TransientWithDismissOnPointerMoveAway">
<Grid Margin="0">
<Slider
MaxHeight="40"
Width="150"
Margin="0"
Padding="0"
IsSnapToTickEnabled="True"
LargeChange="0.05"
Maximum="1"
Minimum="0"
SmallChange="0.01"
TickFrequency="0.05"
ToolTip.Tip="{locale:Locale AudioVolumeTooltip}"
Value="{Binding Volume}" />
</Grid>
</Flyout>
</ui:ToggleSplitButton.Flyout>
</ui:ToggleSplitButton>
<Border
Width="2"
Height="12"
Margin="0"
BorderBrush="Gray"
BorderThickness="1"
IsVisible="{Binding !ShowLoadProgress}" />
<TextBlock
Margin="5,0,5,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsVisible="{Binding !ShowLoadProgress}"
Text="{Binding GameStatusText}"
TextAlignment="Left" />
<Border
Width="2"
Height="12"
Margin="0"
BorderBrush="Gray"
BorderThickness="1"
IsVisible="{Binding !ShowLoadProgress}" />
<TextBlock
Margin="5,0,5,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsVisible="{Binding !ShowLoadProgress}"
Text="{Binding FifoStatusText}"
TextAlignment="Left" />
<Border
Width="2"
Height="12"
Margin="0"
BorderBrush="Gray"
BorderThickness="1"
IsVisible="{Binding !ShowLoadProgress}" />
<TextBlock
Margin="5,0,5,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsVisible="{Binding !ShowLoadProgress}"
Text="{Binding BackendText}"
TextAlignment="Left" />
<Border
Width="2"
Height="12"
Margin="0"
BorderBrush="Gray"
BorderThickness="1"
IsVisible="{Binding !ShowLoadProgress}" />
<TextBlock
Margin="5,0,5,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsVisible="{Binding !ShowLoadProgress}"
Text="{Binding GpuNameText}"
TextAlignment="Left" />
</StackPanel>
<StackPanel
Grid.Column="3"
Margin="0,0,5,0"
VerticalAlignment="Center"
IsVisible="{Binding ShowFirmwareStatus}"
Orientation="Horizontal">
<TextBlock
Name="FirmwareStatus"
Margin="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{locale:Locale StatusBarSystemVersion}" />
</StackPanel>
</Grid>
</UserControl>

View File

@ -0,0 +1,52 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Ui.Common.Configuration;
using System;
namespace Ryujinx.Ava.UI.Views.Main
{
public partial class MainStatusBarView : UserControl
{
public MainWindow Window;
public MainStatusBarView()
{
InitializeComponent();
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (VisualRoot is MainWindow window)
{
Window = window;
}
DataContext = Window.ViewModel;
}
private void VsyncStatus_PointerReleased(object sender, PointerReleasedEventArgs e)
{
Window.ViewModel.AppHost.Device.EnableDeviceVsync = !Window.ViewModel.AppHost.Device.EnableDeviceVsync;
Logger.Info?.Print(LogClass.Application, $"VSync toggled to: {Window.ViewModel.AppHost.Device.EnableDeviceVsync}");
}
private void DockedStatus_PointerReleased(object sender, PointerReleasedEventArgs e)
{
ConfigurationState.Instance.System.EnableDockedMode.Value = !ConfigurationState.Instance.System.EnableDockedMode.Value;
}
private void AspectRatioStatus_PointerReleased(object sender, PointerReleasedEventArgs e)
{
AspectRatio aspectRatio = ConfigurationState.Instance.Graphics.AspectRatio.Value;
ConfigurationState.Instance.Graphics.AspectRatio.Value = (int)aspectRatio + 1 > Enum.GetNames(typeof(AspectRatio)).Length - 1 ? AspectRatio.Fixed4x3 : aspectRatio + 1;
}
}
}

View File

@ -0,0 +1,176 @@
<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:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ryujinx.Ava.UI.Views.Main.MainViewControls"
x:CompileBindings="True"
x:DataType="viewModels:MainWindowViewModel">
<Design.DataContext>
<viewModels:MainWindowViewModel />
</Design.DataContext>
<DockPanel
Margin="0,0,0,5"
HorizontalAlignment="Stretch">
<Button
Width="40"
MinWidth="40"
Margin="5,2,0,2"
VerticalAlignment="Stretch"
Command="{ReflectionBinding SetListMode}"
IsEnabled="{Binding IsGrid}">
<ui:FontIcon
Margin="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
FontFamily="avares://FluentAvalonia/Fonts#Symbols"
Glyph="{helpers:GlyphValueConverter List}" />
</Button>
<Button
Width="40"
MinWidth="40"
Margin="5,2,5,2"
VerticalAlignment="Stretch"
Command="{ReflectionBinding SetGridMode}"
IsEnabled="{Binding IsList}">
<ui:FontIcon
Margin="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
FontFamily="avares://FluentAvalonia/Fonts#Symbols"
Glyph="{helpers:GlyphValueConverter Grid}" />
</Button>
<TextBlock
Margin="10,0"
VerticalAlignment="Center"
Text="{locale:Locale IconSize}"
ToolTip.Tip="{locale:Locale IconSizeTooltip}" />
<Slider
Width="150"
Height="35"
Margin="5,-10,5,0"
VerticalAlignment="Center"
IsSnapToTickEnabled="True"
Maximum="4"
Minimum="1"
TickFrequency="1"
ToolTip.Tip="{locale:Locale IconSizeTooltip}"
Value="{Binding GridSizeScale}" />
<CheckBox
Margin="0"
VerticalAlignment="Center"
IsChecked="{Binding ShowNames, Mode=TwoWay}"
IsVisible="{Binding IsGrid}">
<TextBlock Margin="5,3,0,0" Text="{locale:Locale CommonShowNames}" />
</CheckBox>
<TextBox
Name="SearchBox"
MinWidth="200"
Margin="5,0,5,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
VerticalContentAlignment="Center"
DockPanel.Dock="Right"
KeyUp="SearchBox_OnKeyUp"
Text="{Binding SearchText}"
Watermark="{locale:Locale MenuSearch}" />
<ui:DropDownButton
Width="150"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="{Binding SortName}"
DockPanel.Dock="Right">
<ui:DropDownButton.Flyout>
<Flyout Placement="Bottom">
<StackPanel
Margin="0"
HorizontalAlignment="Stretch"
Orientation="Vertical">
<StackPanel>
<RadioButton
Checked="Sort_Checked"
Content="{locale:Locale CommonFavorite}"
GroupName="Sort"
IsChecked="{Binding IsSortedByFavorite, Mode=OneTime}"
Tag="Favorite" />
<RadioButton
Checked="Sort_Checked"
Content="{locale:Locale GameListHeaderApplication}"
GroupName="Sort"
IsChecked="{Binding IsSortedByTitle, Mode=OneTime}"
Tag="Title" />
<RadioButton
Checked="Sort_Checked"
Content="{locale:Locale GameListHeaderDeveloper}"
GroupName="Sort"
IsChecked="{Binding IsSortedByDeveloper, Mode=OneTime}"
Tag="Developer" />
<RadioButton
Checked="Sort_Checked"
Content="{locale:Locale GameListHeaderTimePlayed}"
GroupName="Sort"
IsChecked="{Binding IsSortedByTimePlayed, Mode=OneTime}"
Tag="TotalTimePlayed" />
<RadioButton
Checked="Sort_Checked"
Content="{locale:Locale GameListHeaderLastPlayed}"
GroupName="Sort"
IsChecked="{Binding IsSortedByLastPlayed, Mode=OneTime}"
Tag="LastPlayed" />
<RadioButton
Checked="Sort_Checked"
Content="{locale:Locale GameListHeaderFileExtension}"
GroupName="Sort"
IsChecked="{Binding IsSortedByType, Mode=OneTime}"
Tag="FileType" />
<RadioButton
Checked="Sort_Checked"
Content="{locale:Locale GameListHeaderFileSize}"
GroupName="Sort"
IsChecked="{Binding IsSortedBySize, Mode=OneTime}"
Tag="FileSize" />
<RadioButton
Checked="Sort_Checked"
Content="{locale:Locale GameListHeaderPath}"
GroupName="Sort"
IsChecked="{Binding IsSortedByPath, Mode=OneTime}"
Tag="Path" />
</StackPanel>
<Border
Width="60"
Height="2"
Margin="5"
HorizontalAlignment="Stretch"
BorderBrush="White"
BorderThickness="0,1,0,0">
<Separator Height="0" HorizontalAlignment="Stretch" />
</Border>
<RadioButton
Checked="Order_Checked"
Content="{locale:Locale OrderAscending}"
GroupName="Order"
IsChecked="{Binding IsAscending, Mode=OneTime}"
Tag="Ascending" />
<RadioButton
Checked="Order_Checked"
Content="{locale:Locale OrderDescending}"
GroupName="Order"
IsChecked="{Binding !IsAscending, Mode=OneTime}"
Tag="Descending" />
</StackPanel>
</Flyout>
</ui:DropDownButton.Flyout>
</ui:DropDownButton>
<TextBlock
Margin="10,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
DockPanel.Dock="Right"
Text="{locale:Locale CommonSort}" />
</DockPanel>
</UserControl>

View File

@ -0,0 +1,54 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Ryujinx.Ava.Common;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows;
using System;
namespace Ryujinx.Ava.UI.Views.Main
{
public partial class MainViewControls : UserControl
{
public MainWindowViewModel ViewModel;
public MainViewControls()
{
InitializeComponent();
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (VisualRoot is MainWindow window)
{
ViewModel = window.ViewModel;
}
DataContext = ViewModel;
}
public void Sort_Checked(object sender, RoutedEventArgs args)
{
if (sender is RadioButton button)
{
ViewModel.Sort(Enum.Parse<ApplicationSort>(button.Tag.ToString()));
}
}
public void Order_Checked(object sender, RoutedEventArgs args)
{
if (sender is RadioButton button)
{
ViewModel.Sort(button.Tag.ToString() != "Descending");
}
}
private void SearchBox_OnKeyUp(object sender, KeyEventArgs e)
{
ViewModel.SearchText = SearchBox.Text;
}
}
}

View File

@ -0,0 +1,81 @@
<UserControl
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsAudioView"
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:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
mc:Ignorable="d"
x:CompileBindings="True"
x:DataType="viewModels:SettingsViewModel">
<Design.DataContext>
<viewModels:SettingsViewModel />
</Design.DataContext>
<ScrollViewer
Name="AudioPage"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Border Classes="settings">
<StackPanel
Margin="10"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabAudio}" />
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
Text="{locale:Locale SettingsTabSystemAudioBackend}"
ToolTip.Tip="{locale:Locale AudioBackendTooltip}"
Width="250" />
<ComboBox SelectedIndex="{Binding AudioBackend}"
Width="350"
HorizontalContentAlignment="Left">
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemAudioBackendDummy}" />
</ComboBoxItem>
<ComboBoxItem IsEnabled="{Binding IsOpenAlEnabled}">
<TextBlock Text="{locale:Locale SettingsTabSystemAudioBackendOpenAL}" />
</ComboBoxItem>
<ComboBoxItem IsEnabled="{Binding IsSoundIoEnabled}">
<TextBlock Text="{locale:Locale SettingsTabSystemAudioBackendSoundIO}" />
</ComboBoxItem>
<ComboBoxItem IsEnabled="{Binding IsSDL2Enabled}">
<TextBlock Text="{locale:Locale SettingsTabSystemAudioBackendSDL2}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
Text="{locale:Locale SettingsTabSystemAudioVolume}"
ToolTip.Tip="{locale:Locale AudioVolumeTooltip}"
Width="250" />
<ui:NumberBox Value="{Binding Volume}"
ToolTip.Tip="{locale:Locale AudioVolumeTooltip}"
Width="350"
SmallChange="1"
LargeChange="10"
SimpleNumberFormat="F0"
SpinButtonPlacementMode="Inline"
Minimum="0"
Maximum="100" />
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<Slider Value="{Binding Volume}"
Margin="250,0,0,0"
ToolTip.Tip="{locale:Locale AudioVolumeTooltip}"
Minimum="0"
Maximum="100"
SmallChange="5"
TickFrequency="5"
IsSnapToTickEnabled="True"
LargeChange="10"
Width="350" />
</StackPanel>
</StackPanel>
</Border>
</ScrollViewer>
</UserControl>

View File

@ -0,0 +1,12 @@
using Avalonia.Controls;
namespace Ryujinx.Ava.UI.Views.Settings
{
public partial class SettingsAudioView : UserControl
{
public SettingsAudioView()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,72 @@
<UserControl
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsCPUView"
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:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
mc:Ignorable="d"
x:CompileBindings="True"
x:DataType="viewModels:SettingsViewModel">
<Design.DataContext>
<viewModels:SettingsViewModel />
</Design.DataContext>
<ScrollViewer
Name="CpuPage"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Border Classes="settings">
<StackPanel
Margin="10"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabCpuCache}" />
<StackPanel
Margin="10,0,0,0"
HorizontalAlignment="Stretch"
Orientation="Vertical">
<CheckBox IsChecked="{Binding EnablePptc}">
<TextBlock Text="{locale:Locale SettingsTabSystemEnablePptc}"
ToolTip.Tip="{locale:Locale PptcToggleTooltip}" />
</CheckBox>
</StackPanel>
<Separator Height="1" />
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabCpuMemory}" />
<StackPanel
Margin="10,0,0,0"
HorizontalAlignment="Stretch"
Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
Text="{locale:Locale SettingsTabSystemMemoryManagerMode}"
ToolTip.Tip="{locale:Locale MemoryManagerTooltip}"
Width="250" />
<ComboBox SelectedIndex="{Binding MemoryMode}"
ToolTip.Tip="{locale:Locale MemoryManagerTooltip}"
HorizontalContentAlignment="Left"
Width="350">
<ComboBoxItem
ToolTip.Tip="{locale:Locale MemoryManagerSoftwareTooltip}">
<TextBlock
Text="{locale:Locale SettingsTabSystemMemoryManagerModeSoftware}" />
</ComboBoxItem>
<ComboBoxItem
ToolTip.Tip="{locale:Locale MemoryManagerHostTooltip}">
<TextBlock Text="{locale:Locale SettingsTabSystemMemoryManagerModeHost}" />
</ComboBoxItem>
<ComboBoxItem
ToolTip.Tip="{locale:Locale MemoryManagerUnsafeTooltip}">
<TextBlock
Text="{locale:Locale SettingsTabSystemMemoryManagerModeHostUnchecked}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
</StackPanel>
</StackPanel>
</Border>
</ScrollViewer>
</UserControl>

View File

@ -0,0 +1,12 @@
using Avalonia.Controls;
namespace Ryujinx.Ava.UI.Views.Settings
{
public partial class SettingsCPUView : UserControl
{
public SettingsCPUView()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,218 @@
<UserControl
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsGraphicsView"
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:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
mc:Ignorable="d"
x:CompileBindings="True"
x:DataType="viewModels:SettingsViewModel">
<Design.DataContext>
<viewModels:SettingsViewModel />
</Design.DataContext>
<ScrollViewer
Name="GraphicsPage"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Border Classes="settings">
<StackPanel
Margin="10"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGraphicsAPI}" />
<StackPanel Margin="10,0,0,0" Orientation="Vertical" Spacing="10">
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale SettingsTabGraphicsBackendTooltip}"
Text="{locale:Locale SettingsTabGraphicsBackend}"
Width="250" />
<ComboBox Width="350"
HorizontalContentAlignment="Left"
ToolTip.Tip="{locale:Locale SettingsTabGraphicsBackendTooltip}"
SelectedIndex="{Binding GraphicsBackendIndex}">
<ComboBoxItem IsVisible="{Binding IsVulkanAvailable}">
<TextBlock Text="Vulkan" />
</ComboBoxItem>
<ComboBoxItem IsEnabled="{Binding IsOpenGLAvailable}">
<TextBlock Text="OpenGL" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
<StackPanel Orientation="Horizontal" IsVisible="{Binding IsVulkanSelected}">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale SettingsTabGraphicsPreferredGpuTooltip}"
Text="{locale:Locale SettingsTabGraphicsPreferredGpu}"
Width="250" />
<ComboBox Width="350"
HorizontalContentAlignment="Left"
ToolTip.Tip="{locale:Locale SettingsTabGraphicsPreferredGpuTooltip}"
SelectedIndex="{Binding PreferredGpuIndex}"
Items="{Binding AvailableGpus}"/>
</StackPanel>
</StackPanel>
<Separator Height="1" />
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGraphicsFeatures}" />
<StackPanel Margin="10,0,0,0" Orientation="Vertical" Spacing="10">
<StackPanel Orientation="Vertical">
<CheckBox IsChecked="{Binding EnableShaderCache}"
ToolTip.Tip="{locale:Locale ShaderCacheToggleTooltip}">
<TextBlock Text="{locale:Locale SettingsTabGraphicsEnableShaderCache}" />
</CheckBox>
<CheckBox IsChecked="{Binding EnableTextureRecompression}"
ToolTip.Tip="{locale:Locale SettingsEnableTextureRecompressionTooltip}">
<TextBlock Text="{locale:Locale SettingsEnableTextureRecompression}" />
</CheckBox>
<CheckBox IsChecked="{Binding EnableMacroHLE}"
ToolTip.Tip="{locale:Locale SettingsEnableMacroHLETooltip}">
<TextBlock Text="{locale:Locale SettingsEnableMacroHLE}" />
</CheckBox>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale ResolutionScaleTooltip}"
Text="{locale:Locale SettingsTabGraphicsResolutionScale}"
Width="250" />
<ComboBox SelectedIndex="{Binding ResolutionScale}"
Width="350"
HorizontalContentAlignment="Left"
ToolTip.Tip="{locale:Locale ResolutionScaleTooltip}">
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScaleCustom}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScaleNative}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScale2x}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScale3x}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScale4x}" />
</ComboBoxItem>
</ComboBox>
<ui:NumberBox
Margin="10,0,0,0"
ToolTip.Tip="{locale:Locale ResolutionScaleEntryTooltip}"
MinWidth="150"
SmallChange="0.1"
LargeChange="1"
SimpleNumberFormat="F2"
SpinButtonPlacementMode="Inline"
IsVisible="{Binding IsCustomResolutionScaleActive}"
Maximum="100"
Minimum="0.1"
Value="{Binding CustomResolutionScale}" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale AnisotropyTooltip}"
Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering}"
Width="250" />
<ComboBox SelectedIndex="{Binding MaxAnisotropy}"
Width="350"
HorizontalContentAlignment="Left"
ToolTip.Tip="{locale:Locale AnisotropyTooltip}">
<ComboBoxItem>
<TextBlock
Text="{locale:Locale SettingsTabGraphicsAnisotropicFilteringAuto}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering2x}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering4x}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering8x}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock
Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering16x}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale AspectRatioTooltip}"
Text="{locale:Locale SettingsTabGraphicsAspectRatio}"
Width="250" />
<ComboBox SelectedIndex="{Binding AspectRatio}"
Width="350"
HorizontalContentAlignment="Left"
ToolTip.Tip="{locale:Locale AspectRatioTooltip}">
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatio4x3}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatio16x9}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatio16x10}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatio21x9}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatio32x9}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatioStretch}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
</StackPanel>
<StackPanel
Margin="10,0,0,0"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale GraphicsBackendThreadingTooltip}"
Text="{locale:Locale SettingsTabGraphicsBackendMultithreading}"
Width="250" />
<ComboBox Width="350"
HorizontalContentAlignment="Left"
ToolTip.Tip="{locale:Locale GalThreadingTooltip}"
SelectedIndex="{Binding GraphicsBackendMultithreadingIndex}">
<ComboBoxItem>
<TextBlock Text="{locale:Locale CommonAuto}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale CommonOff}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale CommonOn}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
</StackPanel>
<Separator Height="1" />
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGraphicsDeveloperOptions}" />
<StackPanel
Margin="10,0,0,0"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale ShaderDumpPathTooltip}"
Text="{locale:Locale SettingsTabGraphicsShaderDumpPath}"
Width="250" />
<TextBox Text="{Binding ShaderDumpPath}"
Width="350"
ToolTip.Tip="{locale:Locale ShaderDumpPathTooltip}" />
</StackPanel>
</StackPanel>
</StackPanel>
</Border>
</ScrollViewer>
</UserControl>

View File

@ -0,0 +1,12 @@
using Avalonia.Controls;
namespace Ryujinx.Ava.UI.Views.Settings
{
public partial class SettingsGraphicsView : UserControl
{
public SettingsGraphicsView()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,104 @@
<UserControl
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsHotkeysView"
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:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
mc:Ignorable="d"
x:CompileBindings="True"
x:DataType="viewModels:SettingsViewModel"
Focusable="True">
<Design.DataContext>
<viewModels:SettingsViewModel />
</Design.DataContext>
<UserControl.Resources>
<helpers:KeyValueConverter x:Key="Key" />
</UserControl.Resources>
<ScrollViewer
Name="HotkeysPage"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Border Classes="settings">
<StackPanel Margin="10" Orientation="Vertical" Spacing="10">
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabHotkeysHotkeys}" />
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysToggleVsyncHotkey}" Width="230" />
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
<TextBlock
Text="{Binding KeyboardHotkeys.ToggleVsync, Mode=TwoWay, Converter={StaticResource Key}}"
TextAlignment="Center" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysScreenshotHotkey}" Width="230" />
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
<TextBlock
Text="{Binding KeyboardHotkeys.Screenshot, Mode=TwoWay, Converter={StaticResource Key}}"
TextAlignment="Center" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysShowUiHotkey}" Width="230" />
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
<TextBlock
Text="{Binding KeyboardHotkeys.ShowUi, Mode=TwoWay, Converter={StaticResource Key}}"
TextAlignment="Center" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysPauseHotkey}" Width="230" />
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
<TextBlock
Text="{Binding KeyboardHotkeys.Pause, Mode=TwoWay, Converter={StaticResource Key}}"
TextAlignment="Center" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysToggleMuteHotkey}" Width="230" />
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
<TextBlock
Text="{Binding KeyboardHotkeys.ToggleMute, Mode=TwoWay, Converter={StaticResource Key}}"
TextAlignment="Center" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysResScaleUpHotkey}" Width="230" />
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
<TextBlock
Text="{Binding KeyboardHotkeys.ResScaleUp, Mode=TwoWay, Converter={StaticResource Key}}"
TextAlignment="Center" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysResScaleDownHotkey}" Width="230" />
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
<TextBlock
Text="{Binding KeyboardHotkeys.ResScaleDown, Mode=TwoWay, Converter={StaticResource Key}}"
TextAlignment="Center" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysVolumeUpHotkey}" Width="230" />
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
<TextBlock
Text="{Binding KeyboardHotkeys.VolumeUp, Mode=TwoWay, Converter={StaticResource Key}}"
TextAlignment="Center" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysVolumeDownHotkey}" Width="230" />
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
<TextBlock
Text="{Binding KeyboardHotkeys.VolumeDown, Mode=TwoWay, Converter={StaticResource Key}}"
TextAlignment="Center" />
</ToggleButton>
</StackPanel>
</StackPanel>
</Border>
</ScrollViewer>
</UserControl>

View File

@ -0,0 +1,81 @@
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Ryujinx.Ava.Input;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Input;
using Ryujinx.Input.Assigner;
namespace Ryujinx.Ava.UI.Views.Settings
{
public partial class SettingsHotkeysView : UserControl
{
private ButtonKeyAssigner _currentAssigner;
private IGamepadDriver AvaloniaKeyboardDriver;
public SettingsHotkeysView()
{
InitializeComponent();
AvaloniaKeyboardDriver = new AvaloniaKeyboardDriver(this);
}
private void MouseClick(object sender, PointerPressedEventArgs e)
{
bool shouldUnbind = e.GetCurrentPoint(this).Properties.IsMiddleButtonPressed;
_currentAssigner?.Cancel(shouldUnbind);
PointerPressed -= MouseClick;
}
private void Button_Checked(object sender, RoutedEventArgs e)
{
if (sender is ToggleButton button)
{
if (_currentAssigner != null && button == _currentAssigner.ToggledButton)
{
return;
}
if (_currentAssigner == null && button.IsChecked != null && (bool)button.IsChecked)
{
_currentAssigner = new ButtonKeyAssigner(button);
FocusManager.Instance?.Focus(this, NavigationMethod.Pointer);
PointerPressed += MouseClick;
var keyboard = (IKeyboard)AvaloniaKeyboardDriver.GetGamepad(AvaloniaKeyboardDriver.GamepadsIds[0]);
IButtonAssigner assigner = new KeyboardKeyAssigner(keyboard);
_currentAssigner.GetInputAndAssign(assigner);
}
else
{
if (_currentAssigner != null)
{
ToggleButton oldButton = _currentAssigner.ToggledButton;
_currentAssigner.Cancel();
_currentAssigner = null;
button.IsChecked = false;
}
}
}
}
private void Button_Unchecked(object sender, RoutedEventArgs e)
{
_currentAssigner?.Cancel();
_currentAssigner = null;
}
public void Dispose()
{
_currentAssigner?.Cancel();
_currentAssigner = null;
}
}
}

View File

@ -0,0 +1,46 @@
<UserControl
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsInputView"
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:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
mc:Ignorable="d"
x:CompileBindings="True"
x:DataType="viewModels:SettingsViewModel">
<Design.DataContext>
<viewModels:SettingsViewModel />
</Design.DataContext>
<ScrollViewer
Name="InputPage"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Border Classes="settings">
<StackPanel Margin="4" Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<CheckBox Margin="5,0"
ToolTip.Tip="{locale:Locale DockModeToggleTooltip}"
IsChecked="{Binding EnableDockedMode}">
<TextBlock VerticalAlignment="Center"
Text="{locale:Locale SettingsTabInputEnableDockedMode}" />
</CheckBox>
<CheckBox Margin="5,0"
ToolTip.Tip="{locale:Locale DirectKeyboardTooltip}"
IsChecked="{Binding EnableKeyboard}">
<TextBlock Text="{locale:Locale SettingsTabInputDirectKeyboardAccess}" />
</CheckBox>
<CheckBox Margin="5,0"
ToolTip.Tip="{locale:Locale DirectMouseTooltip}"
IsChecked="{Binding EnableMouse}">
<TextBlock Text="{locale:Locale SettingsTabInputDirectMouseAccess}" />
</CheckBox>
</StackPanel>
<window:ControllerSettingsWindow Name="ControllerSettings" Margin="0" MinHeight="600" />
</StackPanel>
</Border>
</ScrollViewer>
</UserControl>

View File

@ -0,0 +1,17 @@
using Avalonia.Controls;
namespace Ryujinx.Ava.UI.Views.Settings
{
public partial class SettingsInputView : UserControl
{
public SettingsInputView()
{
InitializeComponent();
}
public void Dispose()
{
ControllerSettings.Dispose();
}
}
}

View File

@ -0,0 +1,118 @@
<UserControl
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsLoggingView"
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:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
mc:Ignorable="d"
x:CompileBindings="True"
x:DataType="viewModels:SettingsViewModel">
<Design.DataContext>
<viewModels:SettingsViewModel />
</Design.DataContext>
<ScrollViewer
Name="LoggingPage"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Border Classes="settings">
<StackPanel
Margin="10"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabLoggingLogging}" />
<StackPanel Margin="10,0,0,0" Orientation="Vertical">
<CheckBox IsChecked="{Binding EnableFileLog}"
ToolTip.Tip="{locale:Locale FileLogTooltip}">
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableLoggingToFile}" />
</CheckBox>
<CheckBox IsChecked="{Binding EnableStub}"
ToolTip.Tip="{locale:Locale StubLogTooltip}">
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableStubLogs}" />
</CheckBox>
<CheckBox IsChecked="{Binding EnableInfo}"
ToolTip.Tip="{locale:Locale InfoLogTooltip}">
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableInfoLogs}" />
</CheckBox>
<CheckBox IsChecked="{Binding EnableWarn}"
ToolTip.Tip="{locale:Locale WarnLogTooltip}">
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableWarningLogs}" />
</CheckBox>
<CheckBox IsChecked="{Binding EnableError}"
ToolTip.Tip="{locale:Locale ErrorLogTooltip}">
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableErrorLogs}" />
</CheckBox>
<CheckBox IsChecked="{Binding EnableTrace}"
ToolTip.Tip="{locale:Locale TraceLogTooltip}">
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableTraceLogs}" />
</CheckBox>
<CheckBox IsChecked="{Binding EnableGuest}"
ToolTip.Tip="{locale:Locale GuestLogTooltip}">
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableGuestLogs}" />
</CheckBox>
</StackPanel>
<Separator Height="1" />
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabLoggingDeveloperOptions}" />
<StackPanel
Margin="10,0,0,0"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
<StackPanel Orientation="Vertical">
<CheckBox IsChecked="{Binding EnableDebug}"
ToolTip.Tip="{locale:Locale DebugLogTooltip}">
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableDebugLogs}" />
</CheckBox>
<CheckBox IsChecked="{Binding EnableFsAccessLog}"
ToolTip.Tip="{locale:Locale FileAccessLogTooltip}">
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableFsAccessLogs}" />
</CheckBox>
<StackPanel Margin="0,10,0,0" Orientation="Horizontal" VerticalAlignment="Stretch">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale FSAccessLogModeTooltip}"
Text="{locale:Locale SettingsTabLoggingFsGlobalAccessLogMode}"
Width="285" />
<ui:NumberBox
Maximum="3"
Minimum="0"
Width="150"
SpinButtonPlacementMode="Inline"
SmallChange="1"
LargeChange="1"
Value="{Binding FsGlobalAccessLogMode}" />
</StackPanel>
<StackPanel Margin="0,10,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
Text="{locale:Locale SettingsTabLoggingGraphicsBackendLogLevel}"
ToolTip.Tip="{locale:Locale OpenGlLogLevel}"
Width="285" />
<ComboBox SelectedIndex="{Binding OpenglDebugLevel}"
Width="150"
HorizontalContentAlignment="Left"
ToolTip.Tip="{locale:Locale OpenGlLogLevel}">
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabLoggingGraphicsBackendLogLevelNone}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabLoggingGraphicsBackendLogLevelError}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock
Text="{locale:Locale SettingsTabLoggingGraphicsBackendLogLevelPerformance}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabLoggingGraphicsBackendLogLevelAll}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
</StackPanel>
</StackPanel>
</StackPanel>
</Border>
</ScrollViewer>
</UserControl>

View File

@ -0,0 +1,12 @@
using Avalonia.Controls;
namespace Ryujinx.Ava.UI.Views.Settings
{
public partial class SettingsLoggingView : UserControl
{
public SettingsLoggingView()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,35 @@
<UserControl
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsNetworkView"
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:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
mc:Ignorable="d"
x:CompileBindings="True"
x:DataType="viewModels:SettingsViewModel">
<Design.DataContext>
<viewModels:SettingsViewModel />
</Design.DataContext>
<ScrollViewer
Name="NetworkPage"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Border Classes="settings">
<StackPanel
Margin="10"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabNetworkConnection}" />
<CheckBox Margin="10,0,0,0" IsChecked="{Binding EnableInternetAccess}">
<TextBlock Text="{locale:Locale SettingsTabSystemEnableInternetAccess}"
ToolTip.Tip="{locale:Locale EnableInternetAccessTooltip}" />
</CheckBox>
</StackPanel>
</Border>
</ScrollViewer>
</UserControl>

View File

@ -0,0 +1,12 @@
using Avalonia.Controls;
namespace Ryujinx.Ava.UI.Views.Settings
{
public partial class SettingsNetworkView : UserControl
{
public SettingsNetworkView()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,195 @@
<UserControl
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsSystemView"
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:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
mc:Ignorable="d"
x:CompileBindings="True"
x:DataType="viewModels:SettingsViewModel">
<Design.DataContext>
<viewModels:SettingsViewModel />
</Design.DataContext>
<ScrollViewer
Name="SystemPage"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Border Classes="settings">
<StackPanel
Margin="10"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabSystemCore}" />
<StackPanel Margin="10,0,0,0" Orientation="Vertical">
<StackPanel Margin="0,0,0,10" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
Text="{locale:Locale SettingsTabSystemSystemRegion}"
Width="250" />
<ComboBox SelectedIndex="{Binding Region}"
ToolTip.Tip="{locale:Locale RegionTooltip}"
HorizontalContentAlignment="Left"
Width="350">
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionJapan}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionUSA}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionEurope}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionAustralia}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionChina}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionKorea}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionTaiwan}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
<StackPanel Margin="0,0,0,10" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
Text="{locale:Locale SettingsTabSystemSystemLanguage}"
ToolTip.Tip="{locale:Locale LanguageTooltip}"
Width="250" />
<ComboBox SelectedIndex="{Binding Language}"
ToolTip.Tip="{locale:Locale LanguageTooltip}"
HorizontalContentAlignment="Left"
Width="350">
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageJapanese}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock
Text="{locale:Locale SettingsTabSystemSystemLanguageAmericanEnglish}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageFrench}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageGerman}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageItalian}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageSpanish}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageChinese}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageKorean}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageDutch}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguagePortuguese}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageRussian}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageTaiwanese}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock
Text="{locale:Locale SettingsTabSystemSystemLanguageBritishEnglish}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock
Text="{locale:Locale SettingsTabSystemSystemLanguageCanadianFrench}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock
Text="{locale:Locale SettingsTabSystemSystemLanguageLatinAmericanSpanish}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock
Text="{locale:Locale SettingsTabSystemSystemLanguageSimplifiedChinese}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock
Text="{locale:Locale SettingsTabSystemSystemLanguageTraditionalChinese}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock
Text="{locale:Locale SettingsTabSystemSystemLanguageBrazilianPortuguese}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
<StackPanel Margin="0,0,0,10" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
Text="{locale:Locale SettingsTabSystemSystemTimeZone}"
ToolTip.Tip="{locale:Locale TimezoneTooltip}"
Width="250" />
<AutoCompleteBox
Name="TimeZoneBox"
Width="350"
MaxDropDownHeight="500"
FilterMode="Contains"
Items="{Binding TimeZones}"
SelectionChanged="TimeZoneBox_OnSelectionChanged"
Text="{Binding Path=TimeZone, Mode=OneWay}"
TextChanged="TimeZoneBox_OnTextChanged"
ToolTip.Tip="{locale:Locale TimezoneTooltip}" />
</StackPanel>
<StackPanel Margin="0,0,0,10" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
Text="{locale:Locale SettingsTabSystemSystemTime}"
ToolTip.Tip="{locale:Locale TimeTooltip}"
Width="250"/>
<DatePicker VerticalAlignment="Center" SelectedDate="{Binding DateOffset}"
ToolTip.Tip="{locale:Locale TimeTooltip}"
Width="350" />
</StackPanel>
<StackPanel Margin="250,0,0,10" Orientation="Horizontal">
<TimePicker
VerticalAlignment="Center"
ClockIdentifier="24HourClock"
SelectedTime="{Binding TimeOffset}"
Width="350"
ToolTip.Tip="{locale:Locale TimeTooltip}" />
</StackPanel>
<CheckBox IsChecked="{Binding EnableVsync}">
<TextBlock Text="{locale:Locale SettingsTabSystemEnableVsync}"
ToolTip.Tip="{locale:Locale VSyncToggleTooltip}" />
</CheckBox>
<CheckBox IsChecked="{Binding EnableFsIntegrityChecks}">
<TextBlock Text="{locale:Locale SettingsTabSystemEnableFsIntegrityChecks}"
ToolTip.Tip="{locale:Locale FsIntegrityToggleTooltip}" />
</CheckBox>
</StackPanel>
<Separator Height="1" />
<StackPanel Orientation="Horizontal">
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabSystemHacks}" />
<TextBlock Text="{locale:Locale SettingsTabSystemHacksNote}" />
</StackPanel>
<StackPanel
Margin="10,0,0,0"
HorizontalAlignment="Stretch"
Orientation="Vertical">
<CheckBox IsChecked="{Binding ExpandDramSize}"
ToolTip.Tip="{locale:Locale DRamTooltip}">
<TextBlock Text="{locale:Locale SettingsTabSystemExpandDramSize}" />
</CheckBox>
<CheckBox IsChecked="{Binding IgnoreMissingServices}"
ToolTip.Tip="{locale:Locale IgnoreMissingServicesTooltip}">
<TextBlock Text="{locale:Locale SettingsTabSystemIgnoreMissingServices}" />
</CheckBox>
</StackPanel>
</StackPanel>
</Border>
</ScrollViewer>
</UserControl>

View File

@ -0,0 +1,52 @@
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Data.Converters;
using Ryujinx.Ava.UI.ViewModels;
using System;
using System.Linq;
using TimeZone = Ryujinx.Ava.UI.Models.TimeZone;
namespace Ryujinx.Ava.UI.Views.Settings
{
public partial class SettingsSystemView : UserControl
{
public SettingsViewModel ViewModel;
public SettingsSystemView()
{
InitializeComponent();
FuncMultiValueConverter<string, string> converter = new(parts => string.Format("{0} {1} {2}", parts.ToArray()).Trim());
MultiBinding tzMultiBinding = new() { Converter = converter };
tzMultiBinding.Bindings.Add(new Binding("UtcDifference"));
tzMultiBinding.Bindings.Add(new Binding("Location"));
tzMultiBinding.Bindings.Add(new Binding("Abbreviation"));
TimeZoneBox.ValueMemberBinding = tzMultiBinding;
}
private void TimeZoneBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems != null && e.AddedItems.Count > 0)
{
if (e.AddedItems[0] is TimeZone timeZone)
{
e.Handled = true;
ViewModel.ValidateAndSetTimeZone(timeZone.Location);
}
}
}
private void TimeZoneBox_OnTextChanged(object sender, EventArgs e)
{
if (sender is AutoCompleteBox box && box.SelectedItem is TimeZone timeZone)
{
{
ViewModel.ValidateAndSetTimeZone(timeZone.Location);
}
}
}
}
}

View File

@ -0,0 +1,156 @@
<UserControl
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsUIView"
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:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
mc:Ignorable="d"
x:CompileBindings="True"
x:DataType="viewModels:SettingsViewModel">
<Design.DataContext>
<viewModels:SettingsViewModel />
</Design.DataContext>
<ScrollViewer
Name="UiPage"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Border Classes="settings">
<StackPanel
Margin="10"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGeneralGeneral}" />
<StackPanel Margin="10,0,0,0" Orientation="Vertical">
<CheckBox IsChecked="{Binding EnableDiscordIntegration}">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale ToggleDiscordTooltip}"
Text="{locale:Locale SettingsTabGeneralEnableDiscordRichPresence}" />
</CheckBox>
<CheckBox IsChecked="{Binding CheckUpdatesOnStart}">
<TextBlock Text="{locale:Locale SettingsTabGeneralCheckUpdatesOnLaunch}" />
</CheckBox>
<CheckBox IsChecked="{Binding ShowConfirmExit}">
<TextBlock Text="{locale:Locale SettingsTabGeneralShowConfirmExitDialog}" />
</CheckBox>
<CheckBox IsChecked="{Binding HideCursorOnIdle}">
<TextBlock Text="{locale:Locale SettingsTabGeneralHideCursorOnIdle}" />
</CheckBox>
</StackPanel>
<Separator Height="1" />
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGeneralGameDirectories}" />
<StackPanel
Margin="10,0,0,0"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
<ListBox
Name="GameList"
MinHeight="230"
Items="{Binding GameDirectories}">
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Padding" Value="10" />
<Setter Property="Background" Value="{DynamicResource ListBoxBackground}" />
</Style>
</ListBox.Styles>
</ListBox>
<Grid HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox
Name="PathBox"
Margin="0"
ToolTip.Tip="{locale:Locale AddGameDirBoxTooltip}"
VerticalAlignment="Stretch" />
<Button
Name="AddButton"
Grid.Column="1"
MinWidth="90"
Margin="10,0,0,0"
ToolTip.Tip="{locale:Locale AddGameDirTooltip}"
Click="AddButton_OnClick">
<TextBlock HorizontalAlignment="Center"
Text="{locale:Locale SettingsTabGeneralAdd}" />
</Button>
<Button
Name="RemoveButton"
Grid.Column="2"
MinWidth="90"
Margin="10,0,0,0"
ToolTip.Tip="{locale:Locale RemoveGameDirTooltip}"
Click="RemoveButton_OnClick">
<TextBlock HorizontalAlignment="Center"
Text="{locale:Locale SettingsTabGeneralRemove}" />
</Button>
</Grid>
</StackPanel>
<Separator Height="1" />
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGeneralTheme}" />
<Grid Margin="10,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<CheckBox
IsChecked="{Binding EnableCustomTheme}"
ToolTip.Tip="{locale:Locale CustomThemeCheckTooltip}">
<TextBlock Text="{locale:Locale SettingsTabGeneralThemeEnableCustomTheme}" />
</CheckBox>
<TextBlock
Grid.Column="0"
Grid.Row="1"
VerticalAlignment="Center"
Margin="0,10,0,0"
Text="{locale:Locale SettingsTabGeneralThemeCustomTheme}"
ToolTip.Tip="{locale:Locale CustomThemePathTooltip}" />
<TextBox
Grid.Row="1"
Grid.Column="1"
Margin="0,10,0,0"
Text="{Binding CustomThemePath}" />
<Button
Grid.Row="1"
Grid.Column="2"
Margin="10,10,0,0"
Click="BrowseTheme"
ToolTip.Tip="{locale:Locale CustomThemeBrowseTooltip}"
Content="{locale:Locale ButtonBrowse}" />
<TextBlock
Grid.Column="0"
Grid.Row="2"
VerticalAlignment="Center"
Margin="0,10,0,0"
Text="{locale:Locale SettingsTabGeneralThemeBaseStyle}" />
<ComboBox
Grid.Column="1"
Grid.Row="2"
VerticalAlignment="Center"
Margin="0,10,0,0"
MinWidth="100"
SelectedIndex="{Binding BaseStyleIndex}">
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGeneralThemeBaseStyleLight}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGeneralThemeBaseStyleDark}" />
</ComboBoxItem>
</ComboBox>
</Grid>
</StackPanel>
</Border>
</ScrollViewer>
</UserControl>

View File

@ -0,0 +1,82 @@
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Interactivity;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.ViewModels;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Ryujinx.Ava.UI.Views.Settings
{
public partial class SettingsUIView : UserControl
{
public SettingsViewModel ViewModel;
public SettingsUIView()
{
InitializeComponent();
}
private async void AddButton_OnClick(object sender, RoutedEventArgs e)
{
string path = PathBox.Text;
if (!string.IsNullOrWhiteSpace(path) && Directory.Exists(path) && !ViewModel.GameDirectories.Contains(path))
{
ViewModel.GameDirectories.Add(path);
ViewModel.DirectoryChanged = true;
}
else
{
if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
path = await new OpenFolderDialog().ShowAsync(desktop.MainWindow);
if (!string.IsNullOrWhiteSpace(path))
{
ViewModel.GameDirectories.Add(path);
ViewModel.DirectoryChanged = true;
}
}
}
}
private void RemoveButton_OnClick(object sender, RoutedEventArgs e)
{
int oldIndex = GameList.SelectedIndex;
foreach (string path in new List<string>(GameList.SelectedItems.Cast<string>()))
{
ViewModel.GameDirectories.Remove(path);
ViewModel.DirectoryChanged = true;
}
if (GameList.ItemCount > 0)
{
GameList.SelectedIndex = oldIndex < GameList.ItemCount ? oldIndex : 0;
}
}
public async void BrowseTheme(object sender, RoutedEventArgs e)
{
var dialog = new OpenFileDialog()
{
Title = LocaleManager.Instance[LocaleKeys.SettingsSelectThemeFileDialogTitle],
AllowMultiple = false
};
dialog.Filters.Add(new FileDialogFilter() { Extensions = { "xaml" }, Name = LocaleManager.Instance[LocaleKeys.SettingsXamlThemeFile] });
if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
var file = await dialog.ShowAsync(desktop.MainWindow);
if (file != null && file.Length > 0)
{
ViewModel.CustomThemePath = file[0];
}
}
}
}
}

View File

@ -1,282 +1,247 @@
<window:StyleableWindow
<UserControl
x:Class="Ryujinx.Ava.UI.Windows.AboutWindow"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:flex="clr-namespace:Avalonia.Flexbox;assembly=Avalonia.Flexbox"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows"
Width="850"
Height="550"
MinWidth="500"
MinHeight="550"
d:DesignHeight="350"
d:DesignWidth="400"
CanResize="False"
SizeToContent="Width"
WindowStartupLocation="CenterOwner"
mc:Ignorable="d"
Focusable="True">
<Grid
Margin="15"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid
Grid.Row="1"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:viewModel="clr-namespace:Ryujinx.Ava.UI.ViewModels"
Width="550"
Height="260"
Margin="0,-12,0,0"
d:DesignHeight="260"
d:DesignWidth="550"
x:CompileBindings="True"
x:DataType="viewModel:AboutWindowViewModel"
Focusable="True"
mc:Ignorable="d">
<Design.DataContext>
<viewModel:AboutWindowViewModel />
</Design.DataContext>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid
Grid.Column="0"
Margin="20"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition />
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Image
Grid.Row="0"
Grid.RowSpan="3"
Grid.Column="0"
Height="110"
MinWidth="50"
Margin="5,10,20,10"
Source="resm:Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png?assembly=Ryujinx.Ui.Common" />
<TextBlock
Grid.Row="0"
Grid.Column="1"
Margin="0,20,0,0"
FontSize="35"
Text="Ryujinx"
TextAlignment="Center" />
<TextBlock
Grid.Row="1"
Grid.Column="1"
Margin="0,0,0,0"
FontSize="16"
Text="(REE-YOU-JINX)"
TextAlignment="Center" />
<Button
Grid.Row="2"
Grid.Column="1"
Margin="0"
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel
Grid.Row="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Spacing="10">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Image
Grid.Column="0"
Height="80"
Source="resm:Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png?assembly=Ryujinx.Ui.Common" />
<flex:FlexPanel
Grid.Column="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Direction="Column"
JustifyContent="SpaceAround"
RowSpacing="2">
<TextBlock
FontSize="28"
FontWeight="Bold"
Text="Ryujinx"
TextAlignment="Left" />
<TextBlock Text="(REE-YOU-JINX)" TextAlignment="Left" />
</flex:FlexPanel>
</Grid>
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="10"
LineHeight="12"
Text="{Binding Version}"
TextAlignment="Center" />
</StackPanel>
<StackPanel
Grid.Row="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Spacing="10">
<TextBlock
Width="200"
HorizontalAlignment="Center"
FontSize="10"
LineHeight="12"
Text="{locale:Locale AboutDisclaimerMessage}"
TextAlignment="Center"
TextWrapping="Wrap" />
<TextBlock
Name="AmiiboLabel"
Width="200"
HorizontalAlignment="Center"
FontSize="10"
LineHeight="12"
PointerPressed="AmiiboLabel_OnPointerPressed"
Text="{locale:Locale AboutAmiiboDisclaimerMessage}"
TextAlignment="Center"
TextWrapping="Wrap" />
<StackPanel
HorizontalAlignment="Center"
Orientation="Horizontal"
Spacing="10">
<Button
MinWidth="30"
MinHeight="30"
MaxWidth="30"
MaxHeight="30"
Padding="8"
Background="Transparent"
Click="Button_OnClick"
CornerRadius="15"
Tag="https://www.patreon.com/ryujinx"
ToolTip.Tip="{locale:Locale AboutPatreonUrlTooltipMessage}">
<Image Source="{Binding PatreonLogo}" />
</Button>
<Button
MinWidth="30"
MinHeight="30"
MaxWidth="30"
MaxHeight="30"
Padding="8"
Background="Transparent"
Click="Button_OnClick"
CornerRadius="15"
Tag="https://github.com/Ryujinx/Ryujinx"
ToolTip.Tip="{locale:Locale AboutGithubUrlTooltipMessage}">
<Image Source="{Binding GithubLogo}" />
</Button>
<Button
MinWidth="30"
MinHeight="30"
MaxWidth="30"
MaxHeight="30"
Padding="8"
Background="Transparent"
Click="Button_OnClick"
CornerRadius="15"
Tag="https://discordapp.com/invite/N2FmfVc"
ToolTip.Tip="{locale:Locale AboutDiscordUrlTooltipMessage}">
<Image Source="{Binding DiscordLogo}" />
</Button>
<Button
MinWidth="30"
MinHeight="30"
MaxWidth="30"
MaxHeight="30"
Padding="8"
Background="Transparent"
Click="Button_OnClick"
CornerRadius="15"
Tag="https://twitter.com/RyujinxEmu"
ToolTip.Tip="{locale:Locale AboutTwitterUrlTooltipMessage}">
<Image Source="{Binding TwitterLogo}" />
</Button>
<Button
MinWidth="30"
MinHeight="30"
MaxWidth="30"
MaxHeight="30"
Padding="8"
Background="Transparent"
Click="Button_OnClick"
CornerRadius="15"
Tag="https://www.ryujinx.org"
ToolTip.Tip="{locale:Locale AboutUrlTooltipMessage}">
<ui:SymbolIcon Foreground="{DynamicResource ThemeForegroundColor}" Symbol="Link" />
</Button>
</StackPanel>
</StackPanel>
</Grid>
<Border
Grid.Column="1"
Width="1"
Margin="20,0"
VerticalAlignment="Stretch"
BorderBrush="{DynamicResource ThemeControlBorderColor}"
BorderThickness="1,0,0,0" />
<Grid
Grid.Column="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel
Grid.Row="0"
Margin="0,10,0,0"
Spacing="2">
<TextBlock
FontSize="15"
FontWeight="Bold"
Text="{locale:Locale AboutRyujinxAboutTitle}" />
<TextBlock
FontSize="10"
Text="{locale:Locale AboutRyujinxAboutContent}"
TextWrapping="Wrap" />
</StackPanel>
<StackPanel
Grid.Row="1"
Margin="0,10,0,0"
Spacing="2">
<TextBlock
FontSize="15"
FontWeight="Bold"
Text="{locale:Locale AboutRyujinxMaintainersTitle}" />
<TextBlock
FontSize="10"
Text="{Binding Developers}"
TextWrapping="Wrap" />
<Button
Padding="5"
HorizontalAlignment="Left"
Background="Transparent"
Click="Button_OnClick"
Tag="https://www.ryujinx.org/">
<TextBlock
Text="www.ryujinx.org"
TextAlignment="Center"
TextDecorations="Underline"
ToolTip.Tip="{locale:Locale AboutUrlTooltipMessage}" />
</Button>
</Grid>
<TextBlock
Grid.Row="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding Version}"
TextAlignment="Center" />
<TextBlock
Grid.Row="2"
Margin="20"
HorizontalAlignment="Center"
MaxLines="2"
Text="{locale:Locale AboutDisclaimerMessage}"
TextAlignment="Center" />
<TextBlock
Name="AmiiboLabel"
Grid.Row="3"
Margin="20"
HorizontalAlignment="Center"
MaxLines="2"
PointerPressed="AmiiboLabel_OnPointerPressed"
Text="{locale:Locale AboutAmiiboDisclaimerMessage}"
TextAlignment="Center" />
<StackPanel
Grid.Row="4"
HorizontalAlignment="Center"
Orientation="Horizontal"
Spacing="10">
<StackPanel Orientation="Vertical" ToolTip.Tip="{locale:Locale AboutPatreonUrlTooltipMessage}">
<Button
Height="65"
Background="Transparent"
Click="Button_OnClick"
Tag="https://www.patreon.com/ryujinx">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Image Source="resm:Ryujinx.Ui.Common.Resources.Logo_Patreon.png?assembly=Ryujinx.Ui.Common" />
<TextBlock
Grid.Row="1"
Margin="0,5,0,0"
HorizontalAlignment="Center"
Text="Patreon" />
</Grid>
</Button>
</StackPanel>
<StackPanel Orientation="Vertical" ToolTip.Tip="{locale:Locale AboutGithubUrlTooltipMessage}">
<Button
Height="65"
Background="Transparent"
Click="Button_OnClick"
Tag="https://github.com/Ryujinx/Ryujinx">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Image Source="resm:Ryujinx.Ui.Common.Resources.Logo_GitHub.png?assembly=Ryujinx.Ui.Common" />
<TextBlock
Grid.Row="1"
Margin="0,5,0,0"
HorizontalAlignment="Center"
Text="GitHub" />
</Grid>
</Button>
</StackPanel>
<StackPanel Orientation="Vertical" ToolTip.Tip="{locale:Locale AboutDiscordUrlTooltipMessage}">
<Button
Height="65"
Background="Transparent"
Click="Button_OnClick"
Tag="https://discordapp.com/invite/N2FmfVc">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Image Source="resm:Ryujinx.Ui.Common.Resources.Logo_Discord.png?assembly=Ryujinx.Ui.Common" />
<TextBlock
Grid.Row="1"
Margin="0,5,0,0"
HorizontalAlignment="Center"
Text="Discord" />
</Grid>
</Button>
</StackPanel>
<StackPanel Orientation="Vertical" ToolTip.Tip="{locale:Locale AboutTwitterUrlTooltipMessage}">
<Button
Height="65"
Background="Transparent"
Click="Button_OnClick"
Tag="https://twitter.com/RyujinxEmu">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Image Source="resm:Ryujinx.Ui.Common.Resources.Logo_Twitter.png?assembly=Ryujinx.Ui.Common" />
<TextBlock
Grid.Row="1"
Margin="0,5,0,0"
HorizontalAlignment="Center"
Text="Twitter" />
</Grid>
</Button>
</StackPanel>
</StackPanel>
</Grid>
<Border
Grid.Row="1"
Grid.Column="1"
Width="2"
Margin="5"
VerticalAlignment="Stretch"
BorderBrush="White"
BorderThickness="1,0,0,0">
<Separator Width="0" />
</Border>
<Grid
Grid.Row="1"
Grid.Column="2"
Margin="20"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock
FontWeight="Bold"
Text="{locale:Locale AboutRyujinxAboutTitle}"
TextDecorations="Underline" />
<TextBlock
Grid.Row="1"
Margin="20,5,5,5"
LineHeight="20"
Text="{locale:Locale AboutRyujinxAboutContent}" />
<TextBlock
Tag="https://github.com/Ryujinx/Ryujinx/graphs/contributors?type=a">
<TextBlock
FontSize="10"
Text="{locale:Locale AboutRyujinxContributorsButtonHeader}"
TextAlignment="Right"
ToolTip.Tip="{locale:Locale AboutRyujinxMaintainersContentTooltipMessage}" />
</Button>
</StackPanel>
<StackPanel
Grid.Row="2"
Margin="0,10,0,0"
FontWeight="Bold"
Text="{locale:Locale AboutRyujinxMaintainersTitle}"
TextDecorations="Underline" />
<TextBlock
Grid.Row="3"
Margin="20,5,5,5"
LineHeight="20"
Text="{Binding Developers}" />
<Button
Grid.Row="4"
HorizontalAlignment="Right"
Background="Transparent"
Click="Button_OnClick"
Tag="https://github.com/Ryujinx/Ryujinx/graphs/contributors?type=a">
<TextBlock
Text="{locale:Locale AboutRyujinxContributorsButtonHeader}"
TextAlignment="Right"
TextDecorations="Underline"
ToolTip.Tip="{locale:Locale AboutRyujinxMaintainersContentTooltipMessage}" />
</Button>
<TextBlock
Grid.Row="5"
Margin="0,0,0,0"
FontWeight="Bold"
Text="{locale:Locale AboutRyujinxSupprtersTitle}"
TextDecorations="Underline" />
<Border
Grid.Row="6"
Width="460"
Height="200"
Margin="20,5"
Padding="5"
VerticalAlignment="Stretch"
BorderBrush="White"
BorderThickness="1">
<TextBlock
Name="SupportersTextBlock"
VerticalAlignment="Top"
Text="{Binding Supporters}"
TextWrapping="Wrap" />
</Border>
</Grid>
</Grid>
</window:StyleableWindow>
Spacing="2">
<TextBlock
FontSize="15"
FontWeight="Bold"
Text="{locale:Locale AboutRyujinxSupprtersTitle}" />
<ScrollViewer
Height="70"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Visible">
<TextBlock
Name="SupportersTextBlock"
VerticalAlignment="Top"
FontSize="10"
Text="{Binding Supporters}"
TextWrapping="Wrap" />
</ScrollViewer>
</StackPanel>
</Grid>
</Grid>
</UserControl>

View File

@ -1,38 +1,47 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Threading;
using Avalonia.Styling;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Common.Utilities;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ui.Common.Helper;
using System.Net.Http;
using System.Net.NetworkInformation;
using System.Threading.Tasks;
using Button = Avalonia.Controls.Button;
namespace Ryujinx.Ava.UI.Windows
{
public partial class AboutWindow : StyleableWindow
public partial class AboutWindow : UserControl
{
public AboutWindow()
{
if (Program.PreviewerDetached)
{
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance[LocaleKeys.MenuBarHelpAbout];
}
Version = Program.Version;
DataContext = this;
DataContext = new AboutWindowViewModel();
InitializeComponent();
_ = DownloadPatronsJson();
}
public string Supporters { get; set; }
public string Version { get; set; }
public static async Task Show()
{
ContentDialog contentDialog = new()
{
PrimaryButtonText = "",
SecondaryButtonText = "",
CloseButtonText = LocaleManager.Instance[LocaleKeys.UserProfilesClose],
Content = new AboutWindow()
};
public string Developers => string.Format(LocaleManager.Instance[LocaleKeys.AboutPageDeveloperListMore], "gdkchan, Ac_K, Thog, rip in peri peri, LDj3SNuD, emmaus, Thealexbarney, Xpl0itR, GoffyDude, »jD«");
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);
}
private void Button_OnClick(object sender, RoutedEventArgs e)
{
@ -42,31 +51,6 @@ namespace Ryujinx.Ava.UI.Windows
}
}
private async Task DownloadPatronsJson()
{
if (!NetworkInterface.GetIsNetworkAvailable())
{
Supporters = LocaleManager.Instance[LocaleKeys.ConnectionError];
return;
}
HttpClient httpClient = new();
try
{
string patreonJsonString = await httpClient.GetStringAsync("https://patreon.ryujinx.org/");
Supporters = string.Join(", ", JsonHelper.Deserialize<string[]>(patreonJsonString));
}
catch
{
Supporters = LocaleManager.Instance[LocaleKeys.ApiError];
}
await Dispatcher.UIThread.InvokeAsync(() => SupportersTextBlock.Text = Supporters);
}
private void AmiiboLabel_OnPointerPressed(object sender, PointerPressedEventArgs e)
{
if (sender is TextBlock)

View File

@ -2,15 +2,16 @@
x:Class="Ryujinx.Ava.UI.Windows.MainWindow"
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:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
Title="Ryujinx"
xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
xmlns:main="clr-namespace:Ryujinx.Ava.UI.Views.Main"
Cursor="{Binding Cursor}"
Title="{Binding Title}"
WindowState="{Binding WindowState}"
Width="1280"
Height="777"
MinWidth="1092"
@ -66,206 +67,8 @@
VerticalAlignment="Stretch"
IsVisible="{Binding ShowMenuAndStatusBar}"
Orientation="Vertical">
<DockPanel HorizontalAlignment="Stretch">
<Menu
Name="Menu"
Height="35"
Margin="0"
HorizontalAlignment="Left">
<Menu.ItemsPanel>
<ItemsPanelTemplate>
<DockPanel Margin="0" HorizontalAlignment="Stretch" />
</ItemsPanelTemplate>
</Menu.ItemsPanel>
<MenuItem VerticalAlignment="Center" Header="{locale:Locale MenuBarFile}">
<MenuItem
Command="{ReflectionBinding OpenFile}"
Header="{locale:Locale MenuBarFileOpenFromFile}"
IsEnabled="{Binding EnableNonGameRunningControls}"
ToolTip.Tip="{locale:Locale LoadApplicationFileTooltip}" />
<MenuItem
Command="{ReflectionBinding OpenFolder}"
Header="{locale:Locale MenuBarFileOpenUnpacked}"
IsEnabled="{Binding EnableNonGameRunningControls}"
ToolTip.Tip="{locale:Locale LoadApplicationFolderTooltip}" />
<MenuItem Header="{locale:Locale MenuBarFileOpenApplet}" IsEnabled="{Binding IsAppletMenuActive}">
<MenuItem
Command="{ReflectionBinding OpenMiiApplet}"
Header="Mii Edit Applet"
ToolTip.Tip="{locale:Locale MenuBarFileOpenAppletOpenMiiAppletToolTip}" />
</MenuItem>
<Separator />
<MenuItem
Command="{ReflectionBinding OpenRyujinxFolder}"
Header="{locale:Locale MenuBarFileOpenEmuFolder}"
ToolTip.Tip="{locale:Locale OpenRyujinxFolderTooltip}" />
<MenuItem
Command="{ReflectionBinding OpenLogsFolder}"
Header="{locale:Locale MenuBarFileOpenLogsFolder}"
ToolTip.Tip="{locale:Locale OpenRyujinxLogsTooltip}" />
<Separator />
<MenuItem
Command="{ReflectionBinding CloseWindow}"
Header="{locale:Locale MenuBarFileExit}"
ToolTip.Tip="{locale:Locale ExitTooltip}" />
</MenuItem>
<MenuItem VerticalAlignment="Center" Header="{locale:Locale MenuBarOptions}">
<MenuItem
Command="{ReflectionBinding ToggleFullscreen}"
Header="{locale:Locale MenuBarOptionsToggleFullscreen}"
InputGesture="F11" />
<MenuItem>
<MenuItem.Icon>
<CheckBox IsChecked="{Binding StartGamesInFullscreen, Mode=TwoWay}"
MinWidth="250">
<TextBlock Text="{locale:Locale MenuBarOptionsStartGamesInFullscreen}"/>
</CheckBox>
</MenuItem.Icon>
</MenuItem>
<MenuItem IsVisible="{Binding ShowConsoleVisible}">
<MenuItem.Icon>
<CheckBox IsChecked="{Binding ShowConsole, Mode=TwoWay}"
MinWidth="250">
<TextBlock Text="{locale:Locale MenuBarOptionsShowConsole}"/>
</CheckBox>
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="{locale:Locale MenuBarOptionsChangeLanguage}">
<MenuItem
Command="{ReflectionBinding ChangeLanguage}"
CommandParameter="de_DE"
Header="Deutsch" />
<MenuItem
Command="{ReflectionBinding ChangeLanguage}"
CommandParameter="en_US"
Header="English (US)" />
<MenuItem
Command="{ReflectionBinding ChangeLanguage}"
CommandParameter="es_ES"
Header="Español (ES)" />
<MenuItem
Command="{ReflectionBinding ChangeLanguage}"
CommandParameter="fr_FR"
Header="Français" />
<MenuItem
Command="{ReflectionBinding ChangeLanguage}"
CommandParameter="it_IT"
Header="Italiano" />
<MenuItem
Command="{ReflectionBinding ChangeLanguage}"
CommandParameter="pt_BR"
Header="Português (BR)" />
<MenuItem
Command="{ReflectionBinding ChangeLanguage}"
CommandParameter="tr_TR"
Header="Türkçe" />
<MenuItem
Command="{ReflectionBinding ChangeLanguage}"
CommandParameter="el_GR"
Header="Ελληνικά" />
<MenuItem
Command="{ReflectionBinding ChangeLanguage}"
CommandParameter="pl_PL"
Header="Polski" />
<MenuItem
Command="{ReflectionBinding ChangeLanguage}"
CommandParameter="ru_RU"
Header="Русский" />
<MenuItem
Command="{ReflectionBinding ChangeLanguage}"
CommandParameter="zh_CN"
Header="简体中文" />
<MenuItem
Command="{ReflectionBinding ChangeLanguage}"
CommandParameter="zh_TW"
Header="繁體中文" />
<MenuItem
Command="{ReflectionBinding ChangeLanguage}"
CommandParameter="ja_JP"
Header="日本語" />
<MenuItem
Command="{ReflectionBinding ChangeLanguage}"
CommandParameter="ko_KR"
Header="한국어" />
</MenuItem>
<Separator />
<MenuItem
Command="{ReflectionBinding OpenSettings}"
Header="{locale:Locale MenuBarOptionsSettings}"
ToolTip.Tip="{locale:Locale OpenSettingsTooltip}" />
<MenuItem
Command="{ReflectionBinding ManageProfiles}"
Header="{locale:Locale MenuBarOptionsManageUserProfiles}"
IsEnabled="{Binding EnableNonGameRunningControls}"
ToolTip.Tip="{locale:Locale OpenProfileManagerTooltip}" />
</MenuItem>
<MenuItem
Name="ActionsMenuItem"
VerticalAlignment="Center"
Header="{locale:Locale MenuBarActions}"
IsEnabled="{Binding IsGameRunning}">
<MenuItem
Click="PauseEmulation_Click"
Header="{locale:Locale MenuBarOptionsPauseEmulation}"
InputGesture="{Binding PauseKey}"
IsEnabled="{Binding !IsPaused}"
IsVisible="{Binding !IsPaused}" />
<MenuItem
Click="ResumeEmulation_Click"
Header="{locale:Locale MenuBarOptionsResumeEmulation}"
InputGesture="{Binding PauseKey}"
IsEnabled="{Binding IsPaused}"
IsVisible="{Binding IsPaused}" />
<MenuItem
Click="StopEmulation_Click"
Header="{locale:Locale MenuBarOptionsStopEmulation}"
InputGesture="Escape"
IsEnabled="{Binding IsGameRunning}"
ToolTip.Tip="{locale:Locale StopEmulationTooltip}" />
<MenuItem Command="{ReflectionBinding SimulateWakeUpMessage}" Header="{locale:Locale MenuBarOptionsSimulateWakeUpMessage}" />
<Separator />
<MenuItem
Name="ScanAmiiboMenuItem"
AttachedToVisualTree="ScanAmiiboMenuItem_AttachedToVisualTree"
Command="{ReflectionBinding OpenAmiiboWindow}"
Header="{locale:Locale MenuBarActionsScanAmiibo}"
IsEnabled="{Binding IsAmiiboRequested}" />
<MenuItem
Command="{ReflectionBinding TakeScreenshot}"
Header="{locale:Locale MenuBarFileToolsTakeScreenshot}"
InputGesture="{Binding ScreenshotKey}"
IsEnabled="{Binding IsGameRunning}" />
<MenuItem
Command="{ReflectionBinding HideUi}"
Header="{locale:Locale MenuBarFileToolsHideUi}"
InputGesture="{Binding ShowUiKey}"
IsEnabled="{Binding IsGameRunning}" />
<MenuItem
Command="{ReflectionBinding OpenCheatManagerForCurrentApp}"
Header="{locale:Locale GameListContextMenuManageCheat}"
IsEnabled="{Binding IsGameRunning}" />
</MenuItem>
<MenuItem VerticalAlignment="Center" Header="{locale:Locale MenuBarTools}">
<MenuItem Header="{locale:Locale MenuBarToolsInstallFirmware}" IsEnabled="{Binding EnableNonGameRunningControls}">
<MenuItem Command="{ReflectionBinding InstallFirmwareFromFile}" Header="{locale:Locale MenuBarFileToolsInstallFirmwareFromFile}" />
<MenuItem Command="{ReflectionBinding InstallFirmwareFromFolder}" Header="{locale:Locale MenuBarFileToolsInstallFirmwareFromDirectory}" />
</MenuItem>
</MenuItem>
<MenuItem VerticalAlignment="Center" Header="{locale:Locale MenuBarHelp}">
<MenuItem
Name="UpdateMenuItem"
Command="{ReflectionBinding CheckForUpdates}"
Header="{locale:Locale MenuBarHelpCheckForUpdates}"
ToolTip.Tip="{locale:Locale CheckUpdatesTooltip}" />
<Separator />
<MenuItem
Command="{ReflectionBinding OpenAboutWindow}"
Header="{locale:Locale MenuBarHelpAbout}"
ToolTip.Tip="{locale:Locale OpenAboutTooltip}" />
</MenuItem>
</Menu>
</DockPanel>
<main:MainMenuBarView
Name="MenuBarView" />
</StackPanel>
<ContentControl
Name="MainContent"
@ -277,170 +80,14 @@
BorderThickness="0,0,0,0"
DockPanel.Dock="Top"
IsVisible="{Binding ShowContent}">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Name="GameLibrary">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<DockPanel
Grid.Row="0"
Margin="0,0,0,5"
HorizontalAlignment="Stretch">
<Button
Width="40"
MinWidth="40"
Margin="5,2,0,2"
VerticalAlignment="Stretch"
Command="{ReflectionBinding SetListMode}"
IsEnabled="{Binding IsGrid}">
<ui:FontIcon
Margin="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
FontFamily="avares://FluentAvalonia/Fonts#Symbols"
Glyph="{helpers:GlyphValueConverter List}" />
</Button>
<Button
Width="40"
MinWidth="40"
Margin="5,2,5,2"
VerticalAlignment="Stretch"
Command="{ReflectionBinding SetGridMode}"
IsEnabled="{Binding IsList}">
<ui:FontIcon
Margin="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
FontFamily="avares://FluentAvalonia/Fonts#Symbols"
Glyph="{helpers:GlyphValueConverter Grid}" />
</Button>
<TextBlock
Margin="10,0"
VerticalAlignment="Center"
Text="{locale:Locale IconSize}"
ToolTip.Tip="{locale:Locale IconSizeTooltip}" />
<Slider
Width="150"
Height="35"
Margin="5,-10,5,0"
VerticalAlignment="Center"
IsSnapToTickEnabled="True"
Maximum="4"
Minimum="1"
TickFrequency="1"
ToolTip.Tip="{locale:Locale IconSizeTooltip}"
Value="{Binding GridSizeScale}" />
<CheckBox
Margin="0"
VerticalAlignment="Center"
IsChecked="{Binding ShowNames, Mode=TwoWay}"
IsVisible="{Binding IsGrid}">
<TextBlock Margin="5,3,0,0" Text="{locale:Locale CommonShowNames}" />
</CheckBox>
<TextBox
Name="SearchBox"
MinWidth="200"
Margin="5,0,5,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
DockPanel.Dock="Right"
KeyUp="SearchBox_OnKeyUp"
Text="{Binding SearchText}"
Watermark="{locale:Locale MenuSearch}" />
<ui:DropDownButton
Width="150"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="{Binding SortName}"
DockPanel.Dock="Right">
<ui:DropDownButton.Flyout>
<Flyout Placement="Bottom">
<StackPanel
Margin="0"
HorizontalAlignment="Stretch"
Orientation="Vertical">
<StackPanel>
<RadioButton
Checked="Sort_Checked"
Content="{locale:Locale CommonFavorite}"
GroupName="Sort"
IsChecked="{Binding IsSortedByFavorite, Mode=OneTime}"
Tag="Favorite" />
<RadioButton
Checked="Sort_Checked"
Content="{locale:Locale GameListHeaderApplication}"
GroupName="Sort"
IsChecked="{Binding IsSortedByTitle, Mode=OneTime}"
Tag="Title" />
<RadioButton
Checked="Sort_Checked"
Content="{locale:Locale GameListHeaderDeveloper}"
GroupName="Sort"
IsChecked="{Binding IsSortedByDeveloper, Mode=OneTime}"
Tag="Developer" />
<RadioButton
Checked="Sort_Checked"
Content="{locale:Locale GameListHeaderTimePlayed}"
GroupName="Sort"
IsChecked="{Binding IsSortedByTimePlayed, Mode=OneTime}"
Tag="TotalTimePlayed" />
<RadioButton
Checked="Sort_Checked"
Content="{locale:Locale GameListHeaderLastPlayed}"
GroupName="Sort"
IsChecked="{Binding IsSortedByLastPlayed, Mode=OneTime}"
Tag="LastPlayed" />
<RadioButton
Checked="Sort_Checked"
Content="{locale:Locale GameListHeaderFileExtension}"
GroupName="Sort"
IsChecked="{Binding IsSortedByType, Mode=OneTime}"
Tag="FileType" />
<RadioButton
Checked="Sort_Checked"
Content="{locale:Locale GameListHeaderFileSize}"
GroupName="Sort"
IsChecked="{Binding IsSortedBySize, Mode=OneTime}"
Tag="FileSize" />
<RadioButton
Checked="Sort_Checked"
Content="{locale:Locale GameListHeaderPath}"
GroupName="Sort"
IsChecked="{Binding IsSortedByPath, Mode=OneTime}"
Tag="Path" />
</StackPanel>
<Border
Width="60"
Height="2"
Margin="5"
HorizontalAlignment="Stretch"
BorderBrush="White"
BorderThickness="0,1,0,0">
<Separator Height="0" HorizontalAlignment="Stretch" />
</Border>
<RadioButton
Checked="Order_Checked"
Content="{locale:Locale OrderAscending}"
GroupName="Order"
IsChecked="{Binding IsAscending, Mode=OneTime}"
Tag="Ascending" />
<RadioButton
Checked="Order_Checked"
Content="{locale:Locale OrderDescending}"
GroupName="Order"
IsChecked="{Binding !IsAscending, Mode=OneTime}"
Tag="Descending" />
</StackPanel>
</Flyout>
</ui:DropDownButton.Flyout>
</ui:DropDownButton>
<TextBlock
Margin="10,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
DockPanel.Dock="Right"
Text="{locale:Locale CommonSort}" />
</DockPanel>
<main:MainViewControls
Name="ViewControls"
Grid.Row="0"/>
<controls:GameListView
x:Name="GameList"
Grid.Row="1"
@ -512,7 +159,8 @@
IsVisible="{Binding ShowLoadProgress}"
Text="{Binding LoadHeading}"
TextAlignment="Left"
TextWrapping="Wrap" />
TextWrapping="Wrap"
MaxWidth="500" />
<Border
Grid.Row="1"
Margin="10"
@ -529,7 +177,6 @@
Margin="0"
Padding="0"
HorizontalAlignment="Stretch"
Background="{Binding ProgressBarBackgroundColor}"
ClipToBounds="True"
CornerRadius="5"
Foreground="{Binding ProgressBarForegroundColor}"
@ -545,226 +192,14 @@
FontSize="18"
IsVisible="{Binding ShowLoadProgress}"
Text="{Binding CacheLoadStatus}"
TextAlignment="Left" />
TextAlignment="Left"
MaxWidth="500" />
</Grid>
</Grid>
</Grid>
<Grid
Name="StatusBar"
Grid.Row="2"
Margin="0"
MinHeight="22"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Background="{DynamicResource ThemeContentBackgroundColor}"
DockPanel.Dock="Bottom"
IsVisible="{Binding ShowMenuAndStatusBar}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel
Grid.Column="0"
Margin="5"
VerticalAlignment="Center"
IsVisible="{Binding EnableNonGameRunningControls}">
<Grid Margin="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Button
Width="25"
Height="25"
MinWidth="0"
Margin="0,0,5,0"
VerticalAlignment="Center"
Background="Transparent"
Command="{ReflectionBinding LoadApplications}">
<ui:SymbolIcon
Width="50"
Height="100"
Symbol="Refresh" />
</Button>
<TextBlock
Name="LoadStatus"
Grid.Column="1"
Margin="0,0,5,0"
VerticalAlignment="Center"
IsVisible="{Binding EnableNonGameRunningControls}"
Text="{locale:Locale StatusBarGamesLoaded}" />
<ProgressBar
Name="LoadProgressBar"
Grid.Column="2"
Height="6"
VerticalAlignment="Center"
Foreground="{DynamicResource HighlightColor}"
IsVisible="{Binding EnableNonGameRunningControls}"
Maximum="{Binding StatusBarProgressMaximum}"
Value="{Binding StatusBarProgressValue}" />
</Grid>
</StackPanel>
<StackPanel
Grid.Column="1"
Margin="0,2"
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsVisible="{Binding IsGameRunning}"
Orientation="Horizontal">
<TextBlock
Name="VsyncStatus"
Margin="5,0,5,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Foreground="{Binding VsyncColor}"
IsVisible="{Binding !ShowLoadProgress}"
PointerReleased="VsyncStatus_PointerReleased"
Text="VSync"
TextAlignment="Left" />
<Border
Width="2"
Height="12"
Margin="0"
BorderBrush="Gray"
BorderThickness="1"
IsVisible="{Binding !ShowLoadProgress}" />
<TextBlock
Name="DockedStatus"
Margin="5,0,5,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsVisible="{Binding !ShowLoadProgress}"
PointerReleased="DockedStatus_PointerReleased"
Text="{Binding DockedStatusText}"
TextAlignment="Left" />
<Border
Width="2"
Height="12"
Margin="0"
BorderBrush="Gray"
BorderThickness="1"
IsVisible="{Binding !ShowLoadProgress}" />
<TextBlock
Name="AspectRatioStatus"
Margin="5,0,5,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsVisible="{Binding !ShowLoadProgress}"
PointerReleased="AspectRatioStatus_PointerReleased"
Text="{Binding AspectRatioStatusText}"
TextAlignment="Left" />
<Border
Width="2"
Height="12"
Margin="0"
BorderBrush="Gray"
BorderThickness="1"
IsVisible="{Binding !ShowLoadProgress}" />
<ui:ToggleSplitButton
Name="VolumeStatus"
Padding="5,0,5,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
VerticalContentAlignment="Center"
Background="{DynamicResource ThemeContentBackgroundColor}"
BorderThickness="0"
Content="{Binding VolumeStatusText}"
IsChecked="{Binding VolumeMuted}"
IsVisible="{Binding !ShowLoadProgress}">
<ui:ToggleSplitButton.Flyout>
<Flyout Placement="Bottom" ShowMode="TransientWithDismissOnPointerMoveAway">
<Grid Margin="0">
<Slider
MaxHeight="40"
Width="150"
Margin="0"
Padding="0"
IsSnapToTickEnabled="True"
LargeChange="0.05"
Maximum="1"
Minimum="0"
SmallChange="0.01"
TickFrequency="0.05"
ToolTip.Tip="{locale:Locale AudioVolumeTooltip}"
Value="{Binding Volume}" />
</Grid>
</Flyout>
</ui:ToggleSplitButton.Flyout>
</ui:ToggleSplitButton>
<Border
Width="2"
Height="12"
Margin="0"
BorderBrush="Gray"
BorderThickness="1"
IsVisible="{Binding !ShowLoadProgress}" />
<TextBlock
Margin="5,0,5,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsVisible="{Binding !ShowLoadProgress}"
Text="{Binding GameStatusText}"
TextAlignment="Left" />
<Border
Width="2"
Height="12"
Margin="0"
BorderBrush="Gray"
BorderThickness="1"
IsVisible="{Binding !ShowLoadProgress}" />
<TextBlock
Margin="5,0,5,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsVisible="{Binding !ShowLoadProgress}"
Text="{Binding FifoStatusText}"
TextAlignment="Left" />
<Border
Width="2"
Height="12"
Margin="0"
BorderBrush="Gray"
BorderThickness="1"
IsVisible="{Binding !ShowLoadProgress}" />
<TextBlock
Margin="5,0,5,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsVisible="{Binding !ShowLoadProgress}"
Text="{Binding BackendText}"
TextAlignment="Left" />
<Border
Width="2"
Height="12"
Margin="0"
BorderBrush="Gray"
BorderThickness="1"
IsVisible="{Binding !ShowLoadProgress}" />
<TextBlock
Margin="5,0,5,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsVisible="{Binding !ShowLoadProgress}"
Text="{Binding GpuNameText}"
TextAlignment="Left" />
</StackPanel>
<StackPanel
Grid.Column="3"
Margin="0,0,5,0"
VerticalAlignment="Center"
IsVisible="{Binding ShowFirmwareStatus}"
Orientation="Horizontal">
<TextBlock
Name="FirmwareStatus"
Margin="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{locale:Locale StatusBarSystemVersion}" />
</StackPanel>
</Grid>
<main:MainStatusBarView
Name="StatusBarView"
Grid.Row="2" />
</Grid>
</Grid>
</window:StyleableWindow>
</window:StyleableWindow>

View File

@ -1,19 +1,14 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Input;
using Ryujinx.Ava.UI.Applet;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.Gpu;
using Ryujinx.HLE.FileSystem;
@ -25,11 +20,9 @@ using Ryujinx.Ui.App.Common;
using Ryujinx.Ui.Common;
using Ryujinx.Ui.Common.Configuration;
using Ryujinx.Ui.Common.Helper;
using SixLabors.ImageSharp.PixelFormats;
using System;
using System.ComponentModel;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using InputManager = Ryujinx.Input.HLE.InputManager;
@ -38,19 +31,14 @@ namespace Ryujinx.Ava.UI.Windows
public partial class MainWindow : StyleableWindow
{
internal static MainWindowViewModel MainWindowViewModel { get; private set; }
private bool _canUpdate;
private bool _isClosing;
private bool _isLoading;
private Control _mainViewContent;
private bool _isLoading;
private UserChannelPersistence _userChannelPersistence;
private static bool _deferLoad;
private static string _launchPath;
private static bool _startFullscreen;
private string _currentEmulatedGamePath;
internal readonly AvaHostUiHandler UiHandler;
private AutoResetEvent _rendererWaitEvent;
public VirtualFileSystem VirtualFileSystem { get; private set; }
public ContentManager ContentManager { get; private set; }
@ -58,30 +46,17 @@ namespace Ryujinx.Ava.UI.Windows
public LibHacHorizonManager LibHacHorizonManager { get; private set; }
internal AppHost AppHost { get; private set; }
public InputManager InputManager { get; private set; }
internal RendererHost RendererControl { get; private set; }
internal MainWindowViewModel ViewModel { get; private set; }
public SettingsWindow SettingsWindow { get; set; }
public bool CanUpdate
{
get => _canUpdate;
set
{
_canUpdate = value;
Dispatcher.UIThread.InvokeAsync(() => UpdateMenuItem.IsEnabled = _canUpdate);
}
}
public static bool ShowKeyErrorOnLoad { get; set; }
public ApplicationLibrary ApplicationLibrary { get; set; }
public MainWindow()
{
ViewModel = new MainWindowViewModel(this);
ViewModel = new MainWindowViewModel();
MainWindowViewModel = ViewModel;
@ -92,10 +67,10 @@ namespace Ryujinx.Ava.UI.Windows
UiHandler = new AvaHostUiHandler(this);
Title = $"Ryujinx {Program.Version}";
ViewModel.Title = $"Ryujinx {Program.Version}";
// NOTE: Height of MenuBar and StatusBar is not usable here, since it would still be 0 at this point.
double barHeight = MenuBar.MinHeight + StatusBar.MinHeight;
double barHeight = MenuBar.MinHeight + StatusBarView.StatusBar.MinHeight;
Height = ((Height - barHeight) / Program.WindowScaleFactor) + barHeight;
Width /= Program.WindowScaleFactor;
@ -103,14 +78,37 @@ namespace Ryujinx.Ava.UI.Windows
{
Initialize();
ViewModel.Initialize();
InputManager = new InputManager(new AvaloniaKeyboardDriver(this), new SDL2GamepadDriver());
ViewModel.Initialize(
ContentManager,
ApplicationLibrary,
VirtualFileSystem,
AccountManager,
InputManager,
_userChannelPersistence,
LibHacHorizonManager,
UiHandler,
ShowLoading,
SwitchToGameControl,
SetMainContent,
this);
ViewModel.RefreshFirmwareStatus();
LoadGameList();
this.GetObservable(IsActiveProperty).Subscribe(IsActiveChanged);
}
_rendererWaitEvent = new AutoResetEvent(false);
ApplicationLibrary.ApplicationCountUpdated += ApplicationLibrary_ApplicationCountUpdated;
ApplicationLibrary.ApplicationAdded += ApplicationLibrary_ApplicationAdded;
ViewModel.ReloadGameList += ReloadGameList;
}
private void IsActiveChanged(bool obj)
{
ViewModel.IsActive = obj;
}
public void LoadGameList()
@ -122,45 +120,51 @@ namespace Ryujinx.Ava.UI.Windows
_isLoading = true;
ViewModel.LoadApplications();
LoadApplications();
_isLoading = false;
}
private void Update_StatusBar(object sender, StatusUpdatedEventArgs args)
{
if (ViewModel.ShowMenuAndStatusBar && !ViewModel.ShowLoadProgress)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
if (args.VSyncEnabled)
{
ViewModel.VsyncColor = new SolidColorBrush(Color.Parse("#ff2eeac9"));
}
else
{
ViewModel.VsyncColor = new SolidColorBrush(Color.Parse("#ffff4554"));
}
ViewModel.DockedStatusText = args.DockedMode;
ViewModel.AspectRatioStatusText = args.AspectRatio;
ViewModel.GameStatusText = args.GameStatus;
ViewModel.VolumeStatusText = args.VolumeStatus;
ViewModel.FifoStatusText = args.FifoStatus;
ViewModel.GpuNameText = args.GpuName;
ViewModel.BackendText = args.GpuBackend;
ViewModel.ShowStatusSeparator = true;
});
}
}
protected override void HandleScalingChanged(double scale)
{
Program.DesktopScaleFactor = scale;
base.HandleScalingChanged(scale);
}
public void AddApplication(ApplicationData applicationData)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
ViewModel.Applications.Add(applicationData);
});
}
private void ApplicationLibrary_ApplicationAdded(object sender, ApplicationAddedEventArgs e)
{
AddApplication(e.AppData);
}
private void ApplicationLibrary_ApplicationCountUpdated(object sender, ApplicationCountUpdatedEventArgs e)
{
LocaleManager.Instance.UpdateDynamicValue(LocaleKeys.StatusBarGamesLoaded, e.NumAppsLoaded, e.NumAppsFound);
Dispatcher.UIThread.Post(() =>
{
ViewModel.StatusBarProgressValue = e.NumAppsLoaded;
ViewModel.StatusBarProgressMaximum = e.NumAppsFound;
if (e.NumAppsFound == 0)
{
StatusBarView.LoadProgressBar.IsVisible = false;
}
if (e.NumAppsLoaded == e.NumAppsFound)
{
StatusBarView.LoadProgressBar.IsVisible = false;
}
});
}
public void Application_Opened(object sender, ApplicationOpenedEventArgs args)
{
if (args.Application != null)
@ -169,50 +173,12 @@ namespace Ryujinx.Ava.UI.Windows
string path = new FileInfo(args.Application.Path).FullName;
LoadApplication(path);
ViewModel.LoadApplication(path);
}
args.Handled = true;
}
public async Task PerformanceCheck()
{
if (ConfigurationState.Instance.Logger.EnableTrace.Value)
{
string mainMessage = LocaleManager.Instance[LocaleKeys.DialogPerformanceCheckLoggingEnabledMessage];
string secondaryMessage = LocaleManager.Instance[LocaleKeys.DialogPerformanceCheckLoggingEnabledConfirmMessage];
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(mainMessage, secondaryMessage,
LocaleManager.Instance[LocaleKeys.InputDialogYes], LocaleManager.Instance[LocaleKeys.InputDialogNo],
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
if (result != UserResult.Yes)
{
ConfigurationState.Instance.Logger.EnableTrace.Value = false;
SaveConfig();
}
}
if (!string.IsNullOrWhiteSpace(ConfigurationState.Instance.Graphics.ShadersDumpPath.Value))
{
string mainMessage = LocaleManager.Instance[LocaleKeys.DialogPerformanceCheckShaderDumpEnabledMessage];
string secondaryMessage =
LocaleManager.Instance[LocaleKeys.DialogPerformanceCheckShaderDumpEnabledConfirmMessage];
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(mainMessage, secondaryMessage,
LocaleManager.Instance[LocaleKeys.InputDialogYes], LocaleManager.Instance[LocaleKeys.InputDialogNo],
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
if (result != UserResult.Yes)
{
ConfigurationState.Instance.Graphics.ShadersDumpPath.Value = "";
SaveConfig();
}
}
}
internal static void DeferLoadApplication(string launchPathArg, bool startFullscreenArg)
{
_deferLoad = true;
@ -220,109 +186,6 @@ namespace Ryujinx.Ava.UI.Windows
_startFullscreen = startFullscreenArg;
}
#pragma warning disable CS1998
public async void LoadApplication(string path, bool startFullscreen = false, string titleName = "")
#pragma warning restore CS1998
{
if (AppHost != null)
{
await ContentDialogHelper.CreateInfoDialog(
LocaleManager.Instance[LocaleKeys.DialogLoadAppGameAlreadyLoadedMessage],
LocaleManager.Instance[LocaleKeys.DialogLoadAppGameAlreadyLoadedSubMessage],
LocaleManager.Instance[LocaleKeys.InputDialogOk],
"",
LocaleManager.Instance[LocaleKeys.RyujinxInfo]);
return;
}
#if RELEASE
await PerformanceCheck();
#endif
Logger.RestartTime();
if (ViewModel.SelectedIcon == null)
{
ViewModel.SelectedIcon = ApplicationLibrary.GetApplicationIcon(path);
}
PrepareLoadScreen();
_mainViewContent = MainContent.Content as Control;
RendererControl = new RendererHost(ConfigurationState.Instance.Logger.GraphicsDebugLevel);
if (ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.OpenGl)
{
RendererControl.CreateOpenGL();
}
else
{
RendererControl.CreateVulkan();
}
AppHost = new AppHost(RendererControl, InputManager, path, VirtualFileSystem, ContentManager, AccountManager, _userChannelPersistence, this);
Dispatcher.UIThread.Post(async () =>
{
if (!await AppHost.LoadGuestApplication())
{
AppHost.DisposeContext();
AppHost = null;
return;
}
CanUpdate = false;
ViewModel.LoadHeading = string.IsNullOrWhiteSpace(titleName) ? string.Format(LocaleManager.Instance[LocaleKeys.LoadingHeading], AppHost.Device.Application.TitleName) : titleName;
ViewModel.TitleName = string.IsNullOrWhiteSpace(titleName) ? AppHost.Device.Application.TitleName : titleName;
SwitchToGameControl(startFullscreen);
_currentEmulatedGamePath = path;
Thread gameThread = new(InitializeGame)
{
Name = "GUI.WindowThread"
};
gameThread.Start();
});
}
private void InitializeGame()
{
RendererControl.RendererInitialized += GlRenderer_Created;
AppHost.StatusUpdatedEvent += Update_StatusBar;
AppHost.AppExit += AppHost_AppExit;
_rendererWaitEvent.WaitOne();
AppHost?.Start();
AppHost.DisposeContext();
}
private void HandleRelaunch()
{
if (_userChannelPersistence.PreviousIndex != -1 && _userChannelPersistence.ShouldRestart)
{
_userChannelPersistence.ShouldRestart = false;
Dispatcher.UIThread.Post(() =>
{
LoadApplication(_currentEmulatedGamePath);
});
}
else
{
// otherwise, clear state.
_userChannelPersistence = new UserChannelPersistence();
_currentEmulatedGamePath = null;
}
}
public void SwitchToGameControl(bool startFullscreen = false)
{
ViewModel.ShowLoadProgress = false;
@ -331,14 +194,10 @@ namespace Ryujinx.Ava.UI.Windows
Dispatcher.UIThread.InvokeAsync(() =>
{
MainContent.Content = RendererControl;
if (startFullscreen && WindowState != WindowState.FullScreen)
if (startFullscreen && ViewModel.WindowState != WindowState.FullScreen)
{
ViewModel.ToggleFullscreen();
}
RendererControl.Focus();
});
}
@ -350,71 +209,16 @@ namespace Ryujinx.Ava.UI.Windows
Dispatcher.UIThread.InvokeAsync(() =>
{
if (startFullscreen && WindowState != WindowState.FullScreen)
if (startFullscreen && ViewModel.WindowState != WindowState.FullScreen)
{
ViewModel.ToggleFullscreen();
}
});
}
private void GlRenderer_Created(object sender, EventArgs e)
{
ShowLoading();
_rendererWaitEvent.Set();
}
private void AppHost_AppExit(object sender, EventArgs e)
{
if (_isClosing)
{
return;
}
ViewModel.IsGameRunning = false;
Dispatcher.UIThread.InvokeAsync(() =>
{
ViewModel.ShowMenuAndStatusBar = true;
ViewModel.ShowContent = true;
ViewModel.ShowLoadProgress = false;
ViewModel.IsLoadingIndeterminate = false;
CanUpdate = true;
Cursor = Cursor.Default;
if (MainContent.Content != _mainViewContent)
{
MainContent.Content = _mainViewContent;
}
AppHost = null;
HandleRelaunch();
});
RendererControl.RendererInitialized -= GlRenderer_Created;
RendererControl = null;
ViewModel.SelectedIcon = null;
Dispatcher.UIThread.InvokeAsync(() =>
{
Title = $"Ryujinx {Program.Version}";
});
}
public void Sort_Checked(object sender, RoutedEventArgs args)
{
if (sender is RadioButton button)
{
var sort = Enum.Parse<ApplicationSort>(button.Tag.ToString());
ViewModel.Sort(sort);
}
}
protected override void HandleWindowStateChanged(WindowState state)
{
WindowState = state;
ViewModel.WindowState = state;
if (state != WindowState.Minimized)
{
@ -422,15 +226,6 @@ namespace Ryujinx.Ava.UI.Windows
}
}
public void Order_Checked(object sender, RoutedEventArgs args)
{
if (sender is RadioButton button)
{
var tag = button.Tag.ToString();
ViewModel.Sort(tag != "Descending");
}
}
private void Initialize()
{
_userChannelPersistence = new UserChannelPersistence();
@ -457,8 +252,6 @@ namespace Ryujinx.Ava.UI.Windows
VirtualFileSystem.ReloadKeySet();
ApplicationHelper.Initialize(VirtualFileSystem, AccountManager, LibHacHorizonManager.RyujinxClient, this);
RefreshFirmwareStatus();
}
protected void CheckLaunchState()
@ -475,7 +268,7 @@ namespace Ryujinx.Ava.UI.Windows
{
_deferLoad = false;
LoadApplication(_launchPath, _startFullscreen);
ViewModel.LoadApplication(_launchPath, _startFullscreen);
}
if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false, this))
@ -487,35 +280,9 @@ namespace Ryujinx.Ava.UI.Windows
}
}
public void RefreshFirmwareStatus()
{
SystemVersion version = null;
try
{
version = ContentManager.GetCurrentFirmwareVersion();
}
catch (Exception) { }
bool hasApplet = false;
if (version != null)
{
LocaleManager.Instance.UpdateDynamicValue(LocaleKeys.StatusBarSystemVersion,
version.VersionString);
hasApplet = version.Major > 3;
}
else
{
LocaleManager.Instance.UpdateDynamicValue(LocaleKeys.StatusBarSystemVersion, "0.0");
}
ViewModel.IsAppletMenuActive = hasApplet;
}
private void Load()
{
VolumeStatus.Click += VolumeStatus_CheckedChanged;
StatusBarView.VolumeStatus.Click += VolumeStatus_CheckedChanged;
GameGrid.ApplicationOpened += Application_Opened;
@ -535,6 +302,19 @@ namespace Ryujinx.Ava.UI.Windows
CheckLaunchState();
}
private void SetMainContent(Control content = null)
{
if (content == null)
{
content = GameLibrary;
}
if (MainContent.Content != content)
{
MainContent.Content = content;
}
}
public static void UpdateGraphicsConfig()
{
GraphicsConfig.ResScale = ConfigurationState.Instance.Graphics.ResScale == -1 ? ConfigurationState.Instance.Graphics.ResScaleCustom : ConfigurationState.Instance.Graphics.ResScale;
@ -553,99 +333,6 @@ namespace Ryujinx.Ava.UI.Windows
HotKeyManager.SetHotKey(ExitHotKey, new KeyGesture(Key.Escape));
}
public static void SaveConfig()
{
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
}
public void UpdateGameMetadata(string titleId)
{
ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata =>
{
if (DateTime.TryParse(appMetadata.LastPlayed, out DateTime lastPlayedDateTime))
{
double sessionTimePlayed = DateTime.UtcNow.Subtract(lastPlayedDateTime).TotalSeconds;
appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero);
}
});
}
private void PrepareLoadScreen()
{
using MemoryStream stream = new MemoryStream(ViewModel.SelectedIcon);
using var gameIconBmp = SixLabors.ImageSharp.Image.Load<Bgra32>(stream);
var dominantColor = IconColorPicker.GetFilteredColor(gameIconBmp).ToPixel<Bgra32>();
const int ColorDivisor = 4;
Color progressFgColor = Color.FromRgb(dominantColor.R, dominantColor.G, dominantColor.B);
Color progressBgColor = Color.FromRgb(
(byte)(dominantColor.R / ColorDivisor),
(byte)(dominantColor.G / ColorDivisor),
(byte)(dominantColor.B / ColorDivisor));
ViewModel.ProgressBarForegroundColor = new SolidColorBrush(progressFgColor);
ViewModel.ProgressBarBackgroundColor = new SolidColorBrush(progressBgColor);
}
private void SearchBox_OnKeyUp(object sender, KeyEventArgs e)
{
ViewModel.SearchText = SearchBox.Text;
}
private async void StopEmulation_Click(object sender, RoutedEventArgs e)
{
if (AppHost != null)
{
await AppHost.ShowExitPrompt();
}
}
private async void PauseEmulation_Click(object sender, RoutedEventArgs e)
{
await Task.Run(() =>
{
AppHost?.Pause();
});
}
private async void ResumeEmulation_Click(object sender, RoutedEventArgs e)
{
await Task.Run(() =>
{
AppHost?.Resume();
});
}
private void ScanAmiiboMenuItem_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
{
if (sender is MenuItem)
{
ViewModel.IsAmiiboRequested = AppHost.Device.System.SearchingForAmiibo(out _);
}
}
private void VsyncStatus_PointerReleased(object sender, PointerReleasedEventArgs e)
{
AppHost.Device.EnableDeviceVsync = !AppHost.Device.EnableDeviceVsync;
Logger.Info?.Print(LogClass.Application, $"VSync toggled to: {AppHost.Device.EnableDeviceVsync}");
}
private void DockedStatus_PointerReleased(object sender, PointerReleasedEventArgs e)
{
ConfigurationState.Instance.System.EnableDockedMode.Value = !ConfigurationState.Instance.System.EnableDockedMode.Value;
}
private void AspectRatioStatus_PointerReleased(object sender, PointerReleasedEventArgs e)
{
AspectRatio aspectRatio = ConfigurationState.Instance.Graphics.AspectRatio.Value;
ConfigurationState.Instance.Graphics.AspectRatio.Value = (int)aspectRatio + 1 > Enum.GetNames(typeof(AspectRatio)).Length - 1 ? AspectRatio.Fixed4x3 : aspectRatio + 1;
}
private void VolumeStatus_CheckedChanged(object sender, SplitButtonClickEventArgs e)
{
var volumeSplitButton = sender as ToggleSplitButton;
@ -653,20 +340,20 @@ namespace Ryujinx.Ava.UI.Windows
{
if (!volumeSplitButton.IsChecked)
{
AppHost.Device.SetVolume(ConfigurationState.Instance.System.AudioVolume);
ViewModel.AppHost.Device.SetVolume(ConfigurationState.Instance.System.AudioVolume);
}
else
{
AppHost.Device.SetVolume(0);
ViewModel.AppHost.Device.SetVolume(0);
}
ViewModel.Volume = AppHost.Device.GetVolume();
ViewModel.Volume = ViewModel.AppHost.Device.GetVolume();
}
}
protected override void OnClosing(CancelEventArgs e)
{
if (!_isClosing && AppHost != null && ConfigurationState.Instance.ShowConfirmExit)
if (!ViewModel.IsClosing && ViewModel.AppHost != null && ConfigurationState.Instance.ShowConfirmExit)
{
e.Cancel = true;
@ -675,14 +362,14 @@ namespace Ryujinx.Ava.UI.Windows
return;
}
_isClosing = true;
ViewModel.IsClosing = true;
if (AppHost != null)
if (ViewModel.AppHost != null)
{
AppHost.AppExit -= AppHost_AppExit;
AppHost.AppExit += (sender, e) =>
ViewModel.AppHost.AppExit -= ViewModel.AppHost_AppExit;
ViewModel.AppHost.AppExit += (sender, e) =>
{
AppHost = null;
ViewModel.AppHost = null;
Dispatcher.UIThread.Post(() =>
{
@ -691,7 +378,7 @@ namespace Ryujinx.Ava.UI.Windows
Close();
});
};
AppHost?.Stop();
ViewModel.AppHost?.Stop();
e.Cancel = true;
@ -709,13 +396,43 @@ namespace Ryujinx.Ava.UI.Windows
{
Dispatcher.UIThread.InvokeAsync(async () =>
{
_isClosing = await ContentDialogHelper.CreateExitDialog();
ViewModel.IsClosing = await ContentDialogHelper.CreateExitDialog();
if (_isClosing)
if (ViewModel.IsClosing)
{
Close();
}
});
}
public async void LoadApplications()
{
await Dispatcher.UIThread.InvokeAsync(() =>
{
ViewModel.Applications.Clear();
StatusBarView.LoadProgressBar.IsVisible = true;
ViewModel.StatusBarProgressMaximum = 0;
ViewModel.StatusBarProgressValue = 0;
LocaleManager.Instance.UpdateDynamicValue(LocaleKeys.StatusBarGamesLoaded, 0, 0);
});
ReloadGameList();
}
private void ReloadGameList()
{
if (_isLoading)
{
return;
}
_isLoading = true;
ApplicationLibrary.LoadApplications(ConfigurationState.Instance.Ui.GameDirs.Value, ConfigurationState.Instance.System.Language);
_isLoading = false;
}
}
}

View File

@ -6,13 +6,12 @@
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
xmlns:settings="clr-namespace:Ryujinx.Ava.UI.Views.Settings"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
Width="1100"
Height="768"
d:DesignWidth="800"
d:DesignHeight="950"
MinWidth="800"
MinHeight="480"
WindowStartupLocation="CenterOwner"
@ -23,9 +22,6 @@
<Design.DataContext>
<viewModels:SettingsViewModel />
</Design.DataContext>
<Window.Resources>
<helpers:KeyValueConverter x:Key="Key" />
</Window.Resources>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" MinWidth="600">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
@ -38,871 +34,15 @@
IsVisible="False"
KeyboardNavigation.IsTabStop="False"/>
<Grid Name="Pages" IsVisible="False" Grid.Row="2">
<ScrollViewer Name="UiPage"
Margin="0,0,2,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Border Classes="settings">
<StackPanel
Margin="10,5"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGeneralGeneral}" />
<StackPanel Margin="10,0,0,0" Orientation="Vertical">
<CheckBox IsChecked="{Binding EnableDiscordIntegration}">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale ToggleDiscordTooltip}"
Text="{locale:Locale SettingsTabGeneralEnableDiscordRichPresence}" />
</CheckBox>
<CheckBox IsChecked="{Binding CheckUpdatesOnStart}">
<TextBlock Text="{locale:Locale SettingsTabGeneralCheckUpdatesOnLaunch}" />
</CheckBox>
<CheckBox IsChecked="{Binding ShowConfirmExit}">
<TextBlock Text="{locale:Locale SettingsTabGeneralShowConfirmExitDialog}" />
</CheckBox>
<CheckBox IsChecked="{Binding HideCursorOnIdle}">
<TextBlock Text="{locale:Locale SettingsTabGeneralHideCursorOnIdle}" />
</CheckBox>
</StackPanel>
<Separator Height="1" />
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGeneralGameDirectories}" />
<StackPanel
Margin="10,0,0,0"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
<ListBox
Name="GameList"
MinHeight="250"
Items="{Binding GameDirectories}" />
<Grid HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox
Name="PathBox"
Margin="0"
ToolTip.Tip="{locale:Locale AddGameDirBoxTooltip}"
VerticalAlignment="Stretch" />
<Button
Name="AddButton"
Grid.Column="1"
MinWidth="90"
Margin="10,0,0,0"
ToolTip.Tip="{locale:Locale AddGameDirTooltip}"
Click="AddButton_OnClick">
<TextBlock HorizontalAlignment="Center"
Text="{locale:Locale SettingsTabGeneralAdd}" />
</Button>
<Button
Name="RemoveButton"
Grid.Column="2"
MinWidth="90"
Margin="10,0,0,0"
ToolTip.Tip="{locale:Locale RemoveGameDirTooltip}"
Click="RemoveButton_OnClick">
<TextBlock HorizontalAlignment="Center"
Text="{locale:Locale SettingsTabGeneralRemove}" />
</Button>
</Grid>
</StackPanel>
<Separator Height="1" />
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGeneralTheme}" />
<Grid Margin="10,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<CheckBox IsChecked="{Binding EnableCustomTheme}"
ToolTip.Tip="{locale:Locale CustomThemeCheckTooltip}">
<TextBlock Text="{locale:Locale SettingsTabGeneralThemeEnableCustomTheme}" />
</CheckBox>
<TextBlock VerticalAlignment="Center"
Margin="0,10,0,0"
Grid.Row="1"
Text="{locale:Locale SettingsTabGeneralThemeCustomTheme}"
ToolTip.Tip="{locale:Locale CustomThemePathTooltip}" />
<TextBox Margin="0,10,0,0"
Grid.Row="1"
Grid.Column="1"
Text="{Binding CustomThemePath}" />
<Button Grid.Row="1"
Grid.Column="2"
Margin="10,10,0,0"
Command="{ReflectionBinding BrowseTheme}"
ToolTip.Tip="{locale:Locale CustomThemeBrowseTooltip}"
Content="{locale:Locale ButtonBrowse}" />
<TextBlock VerticalAlignment="Center"
Margin="0,10,0,0"
Grid.Row="2"
Text="{locale:Locale SettingsTabGeneralThemeBaseStyle}" />
<ComboBox VerticalAlignment="Center"
Margin="0,10,0,0"
Grid.Column="1"
Grid.Row="2"
MinWidth="100"
SelectedIndex="{Binding BaseStyleIndex}">
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGeneralThemeBaseStyleLight}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGeneralThemeBaseStyleDark}" />
</ComboBoxItem>
</ComboBox>
</Grid>
</StackPanel>
</Border>
</ScrollViewer>
<ScrollViewer Name="InputPage"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Padding="0,0,2,0"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Border Classes="settings">
<StackPanel Margin="4" Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<CheckBox Margin="5,0"
ToolTip.Tip="{locale:Locale DockModeToggleTooltip}"
IsChecked="{Binding EnableDockedMode}">
<TextBlock VerticalAlignment="Center"
Text="{locale:Locale SettingsTabInputEnableDockedMode}" />
</CheckBox>
<CheckBox Margin="5,0"
ToolTip.Tip="{locale:Locale DirectKeyboardTooltip}"
IsChecked="{Binding EnableKeyboard}">
<TextBlock Text="{locale:Locale SettingsTabInputDirectKeyboardAccess}" />
</CheckBox>
<CheckBox Margin="5,0"
ToolTip.Tip="{locale:Locale DirectMouseTooltip}"
IsChecked="{Binding EnableMouse}">
<TextBlock Text="{locale:Locale SettingsTabInputDirectMouseAccess}" />
</CheckBox>
</StackPanel>
<window:ControllerSettingsWindow Name="ControllerSettings" Margin="0,0,0,0" MinHeight="600" />
</StackPanel>
</Border>
</ScrollViewer>
<ScrollViewer Name="HotkeysPage"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Border Classes="settings">
<StackPanel Margin="10,5" Orientation="Vertical" Spacing="10">
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabHotkeysHotkeys}" />
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysToggleVsyncHotkey}" Width="230" />
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
<TextBlock
Text="{Binding KeyboardHotkeys.ToggleVsync, Mode=TwoWay, Converter={StaticResource Key}}"
TextAlignment="Center" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysScreenshotHotkey}" Width="230" />
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
<TextBlock
Text="{Binding KeyboardHotkeys.Screenshot, Mode=TwoWay, Converter={StaticResource Key}}"
TextAlignment="Center" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysShowUiHotkey}" Width="230" />
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
<TextBlock
Text="{Binding KeyboardHotkeys.ShowUi, Mode=TwoWay, Converter={StaticResource Key}}"
TextAlignment="Center" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysPauseHotkey}" Width="230" />
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
<TextBlock
Text="{Binding KeyboardHotkeys.Pause, Mode=TwoWay, Converter={StaticResource Key}}"
TextAlignment="Center" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysToggleMuteHotkey}" Width="230" />
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
<TextBlock
Text="{Binding KeyboardHotkeys.ToggleMute, Mode=TwoWay, Converter={StaticResource Key}}"
TextAlignment="Center" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysResScaleUpHotkey}" Width="230" />
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
<TextBlock
Text="{Binding KeyboardHotkeys.ResScaleUp, Mode=TwoWay, Converter={StaticResource Key}}"
TextAlignment="Center" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysResScaleDownHotkey}" Width="230" />
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
<TextBlock
Text="{Binding KeyboardHotkeys.ResScaleDown, Mode=TwoWay, Converter={StaticResource Key}}"
TextAlignment="Center" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysVolumeUpHotkey}" Width="230" />
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
<TextBlock
Text="{Binding KeyboardHotkeys.VolumeUp, Mode=TwoWay, Converter={StaticResource Key}}"
TextAlignment="Center" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysVolumeDownHotkey}" Width="230" />
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
<TextBlock
Text="{Binding KeyboardHotkeys.VolumeDown, Mode=TwoWay, Converter={StaticResource Key}}"
TextAlignment="Center" />
</ToggleButton>
</StackPanel>
</StackPanel>
</Border>
</ScrollViewer>
<ScrollViewer Name="SystemPage"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Border Classes="settings">
<StackPanel
Margin="10,5"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabSystemCore}" />
<StackPanel Margin="10,0,0,0" Orientation="Vertical">
<StackPanel Margin="0,0,0,10" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
Text="{locale:Locale SettingsTabSystemSystemRegion}"
Width="250" />
<ComboBox SelectedIndex="{Binding Region}"
ToolTip.Tip="{locale:Locale RegionTooltip}"
HorizontalContentAlignment="Left"
Width="350">
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionJapan}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionUSA}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionEurope}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionAustralia}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionChina}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionKorea}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionTaiwan}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
<StackPanel Margin="0,0,0,10" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
Text="{locale:Locale SettingsTabSystemSystemLanguage}"
ToolTip.Tip="{locale:Locale LanguageTooltip}"
Width="250" />
<ComboBox SelectedIndex="{Binding Language}"
ToolTip.Tip="{locale:Locale LanguageTooltip}"
HorizontalContentAlignment="Left"
Width="350">
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageJapanese}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock
Text="{locale:Locale SettingsTabSystemSystemLanguageAmericanEnglish}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageFrench}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageGerman}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageItalian}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageSpanish}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageChinese}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageKorean}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageDutch}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguagePortuguese}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageRussian}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageTaiwanese}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock
Text="{locale:Locale SettingsTabSystemSystemLanguageBritishEnglish}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock
Text="{locale:Locale SettingsTabSystemSystemLanguageCanadianFrench}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock
Text="{locale:Locale SettingsTabSystemSystemLanguageLatinAmericanSpanish}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock
Text="{locale:Locale SettingsTabSystemSystemLanguageSimplifiedChinese}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock
Text="{locale:Locale SettingsTabSystemSystemLanguageTraditionalChinese}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock
Text="{locale:Locale SettingsTabSystemSystemLanguageBrazilianPortuguese}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
<StackPanel Margin="0,0,0,10" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
Text="{locale:Locale SettingsTabSystemSystemTimeZone}"
ToolTip.Tip="{locale:Locale TimezoneTooltip}"
Width="250" />
<AutoCompleteBox
Name="TimeZoneBox"
Width="350"
MaxDropDownHeight="500"
FilterMode="Contains"
Items="{Binding TimeZones}"
SelectionChanged="TimeZoneBox_OnSelectionChanged"
Text="{Binding Path=TimeZone, Mode=OneWay}"
TextChanged="TimeZoneBox_OnTextChanged"
ValueMemberBinding="{ReflectionBinding TzMultiBinding}"
ToolTip.Tip="{locale:Locale TimezoneTooltip}" />
</StackPanel>
<StackPanel Margin="0,0,0,10" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
Text="{locale:Locale SettingsTabSystemSystemTime}"
ToolTip.Tip="{locale:Locale TimeTooltip}"
Width="250"/>
<DatePicker VerticalAlignment="Center" SelectedDate="{Binding DateOffset}"
ToolTip.Tip="{locale:Locale TimeTooltip}"
Width="350" />
</StackPanel>
<StackPanel Margin="250,0,0,10" Orientation="Horizontal">
<TimePicker
VerticalAlignment="Center"
ClockIdentifier="24HourClock"
SelectedTime="{Binding TimeOffset}"
Width="350"
ToolTip.Tip="{locale:Locale TimeTooltip}" />
</StackPanel>
<CheckBox IsChecked="{Binding EnableVsync}">
<TextBlock Text="{locale:Locale SettingsTabSystemEnableVsync}"
ToolTip.Tip="{locale:Locale VSyncToggleTooltip}" />
</CheckBox>
<CheckBox IsChecked="{Binding EnableFsIntegrityChecks}">
<TextBlock Text="{locale:Locale SettingsTabSystemEnableFsIntegrityChecks}"
ToolTip.Tip="{locale:Locale FsIntegrityToggleTooltip}" />
</CheckBox>
</StackPanel>
<Separator Height="1" />
<StackPanel Orientation="Horizontal">
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabSystemHacks}" />
<TextBlock Text="{locale:Locale SettingsTabSystemHacksNote}" />
</StackPanel>
<StackPanel
Margin="10,0,0,0"
HorizontalAlignment="Stretch"
Orientation="Vertical">
<CheckBox IsChecked="{Binding ExpandDramSize}"
ToolTip.Tip="{locale:Locale DRamTooltip}">
<TextBlock Text="{locale:Locale SettingsTabSystemExpandDramSize}" />
</CheckBox>
<CheckBox IsChecked="{Binding IgnoreMissingServices}"
ToolTip.Tip="{locale:Locale IgnoreMissingServicesTooltip}">
<TextBlock Text="{locale:Locale SettingsTabSystemIgnoreMissingServices}" />
</CheckBox>
</StackPanel>
</StackPanel>
</Border>
</ScrollViewer>
<ScrollViewer
Name="CpuPage"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Border Classes="settings">
<StackPanel
Margin="10,5"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabCpuCache}" />
<StackPanel
Margin="10,0,0,0"
HorizontalAlignment="Stretch"
Orientation="Vertical">
<CheckBox IsChecked="{Binding EnablePptc}">
<TextBlock Text="{locale:Locale SettingsTabSystemEnablePptc}"
ToolTip.Tip="{locale:Locale PptcToggleTooltip}" />
</CheckBox>
</StackPanel>
<Separator Height="1" />
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabCpuMemory}" />
<StackPanel
Margin="10,0,0,0"
HorizontalAlignment="Stretch"
Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
Text="{locale:Locale SettingsTabSystemMemoryManagerMode}"
ToolTip.Tip="{locale:Locale MemoryManagerTooltip}"
Width="250" />
<ComboBox SelectedIndex="{Binding MemoryMode}"
ToolTip.Tip="{locale:Locale MemoryManagerTooltip}"
HorizontalContentAlignment="Left"
Width="350">
<ComboBoxItem
ToolTip.Tip="{locale:Locale MemoryManagerSoftwareTooltip}">
<TextBlock
Text="{locale:Locale SettingsTabSystemMemoryManagerModeSoftware}" />
</ComboBoxItem>
<ComboBoxItem
ToolTip.Tip="{locale:Locale MemoryManagerHostTooltip}">
<TextBlock Text="{locale:Locale SettingsTabSystemMemoryManagerModeHost}" />
</ComboBoxItem>
<ComboBoxItem
ToolTip.Tip="{locale:Locale MemoryManagerUnsafeTooltip}">
<TextBlock
Text="{locale:Locale SettingsTabSystemMemoryManagerModeHostUnchecked}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
</StackPanel>
</StackPanel>
</Border>
</ScrollViewer>
<ScrollViewer
Name="GraphicsPage"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Border Classes="settings">
<StackPanel
Margin="10,5"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGraphicsAPI}" />
<StackPanel Margin="10,0,0,0" Orientation="Vertical" Spacing="10">
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale SettingsTabGraphicsBackendTooltip}"
Text="{locale:Locale SettingsTabGraphicsBackend}"
Width="250" />
<ComboBox Width="350"
HorizontalContentAlignment="Left"
ToolTip.Tip="{locale:Locale SettingsTabGraphicsBackendTooltip}"
SelectedIndex="{Binding GraphicsBackendIndex}">
<ComboBoxItem IsVisible="{Binding IsVulkanAvailable}">
<TextBlock Text="Vulkan" />
</ComboBoxItem>
<ComboBoxItem IsEnabled="{Binding IsOpenGLAvailable}">
<TextBlock Text="OpenGL" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
<StackPanel Orientation="Horizontal" IsVisible="{Binding IsVulkanSelected}">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale SettingsTabGraphicsPreferredGpuTooltip}"
Text="{locale:Locale SettingsTabGraphicsPreferredGpu}"
Width="250" />
<ComboBox Width="350"
HorizontalContentAlignment="Left"
ToolTip.Tip="{locale:Locale SettingsTabGraphicsPreferredGpuTooltip}"
SelectedIndex="{Binding PreferredGpuIndex}"
Items="{Binding AvailableGpus}"/>
</StackPanel>
</StackPanel>
<Separator Height="1" />
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGraphicsFeatures}" />
<StackPanel Margin="10,0,0,0" Orientation="Vertical" Spacing="10">
<StackPanel Orientation="Vertical">
<CheckBox IsChecked="{Binding EnableShaderCache}"
ToolTip.Tip="{locale:Locale ShaderCacheToggleTooltip}">
<TextBlock Text="{locale:Locale SettingsTabGraphicsEnableShaderCache}" />
</CheckBox>
<CheckBox IsChecked="{Binding EnableTextureRecompression}"
ToolTip.Tip="{locale:Locale SettingsEnableTextureRecompressionTooltip}">
<TextBlock Text="{locale:Locale SettingsEnableTextureRecompression}" />
</CheckBox>
<CheckBox IsChecked="{Binding EnableMacroHLE}"
ToolTip.Tip="{locale:Locale SettingsEnableMacroHLETooltip}">
<TextBlock Text="{locale:Locale SettingsEnableMacroHLE}" />
</CheckBox>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale ResolutionScaleTooltip}"
Text="{locale:Locale SettingsTabGraphicsResolutionScale}"
Width="250" />
<ComboBox SelectedIndex="{Binding ResolutionScale}"
Width="350"
HorizontalContentAlignment="Left"
ToolTip.Tip="{locale:Locale ResolutionScaleTooltip}">
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScaleCustom}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScaleNative}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScale2x}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScale3x}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScale4x}" />
</ComboBoxItem>
</ComboBox>
<ui:NumberBox
Margin="10,0,0,0"
ToolTip.Tip="{locale:Locale ResolutionScaleEntryTooltip}"
MinWidth="150"
SmallChange="0.1"
LargeChange="1"
SimpleNumberFormat="F2"
SpinButtonPlacementMode="Inline"
IsVisible="{Binding IsCustomResolutionScaleActive}"
Maximum="100"
Minimum="0.1"
Value="{Binding CustomResolutionScale}" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale AnisotropyTooltip}"
Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering}"
Width="250" />
<ComboBox SelectedIndex="{Binding MaxAnisotropy}"
Width="350"
HorizontalContentAlignment="Left"
ToolTip.Tip="{locale:Locale AnisotropyTooltip}">
<ComboBoxItem>
<TextBlock
Text="{locale:Locale SettingsTabGraphicsAnisotropicFilteringAuto}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering2x}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering4x}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering8x}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock
Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering16x}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale AspectRatioTooltip}"
Text="{locale:Locale SettingsTabGraphicsAspectRatio}"
Width="250" />
<ComboBox SelectedIndex="{Binding AspectRatio}"
Width="350"
HorizontalContentAlignment="Left"
ToolTip.Tip="{locale:Locale AspectRatioTooltip}">
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatio4x3}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatio16x9}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatio16x10}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatio21x9}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatio32x9}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatioStretch}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
</StackPanel>
<StackPanel
Margin="10,0,0,0"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale GraphicsBackendThreadingTooltip}"
Text="{locale:Locale SettingsTabGraphicsBackendMultithreading}"
Width="250" />
<ComboBox Width="350"
HorizontalContentAlignment="Left"
ToolTip.Tip="{locale:Locale GalThreadingTooltip}"
SelectedIndex="{Binding GraphicsBackendMultithreadingIndex}">
<ComboBoxItem>
<TextBlock Text="{locale:Locale CommonAuto}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale CommonOff}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale CommonOn}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
</StackPanel>
<Separator Height="1" />
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGraphicsDeveloperOptions}" />
<StackPanel
Margin="10,0,0,0"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale ShaderDumpPathTooltip}"
Text="{locale:Locale SettingsTabGraphicsShaderDumpPath}"
Width="250" />
<TextBox Text="{Binding ShaderDumpPath}"
Width="350"
ToolTip.Tip="{locale:Locale ShaderDumpPathTooltip}" />
</StackPanel>
</StackPanel>
</StackPanel>
</Border>
</ScrollViewer>
<ScrollViewer
Name="AudioPage"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Border Classes="settings">
<StackPanel
Margin="10,5"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabAudio}" />
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
Text="{locale:Locale SettingsTabSystemAudioBackend}"
ToolTip.Tip="{locale:Locale AudioBackendTooltip}"
Width="250" />
<ComboBox SelectedIndex="{Binding AudioBackend}"
Width="350"
HorizontalContentAlignment="Left">
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabSystemAudioBackendDummy}" />
</ComboBoxItem>
<ComboBoxItem IsEnabled="{Binding IsOpenAlEnabled}">
<TextBlock Text="{locale:Locale SettingsTabSystemAudioBackendOpenAL}" />
</ComboBoxItem>
<ComboBoxItem IsEnabled="{Binding IsSoundIoEnabled}">
<TextBlock Text="{locale:Locale SettingsTabSystemAudioBackendSoundIO}" />
</ComboBoxItem>
<ComboBoxItem IsEnabled="{Binding IsSDL2Enabled}">
<TextBlock Text="{locale:Locale SettingsTabSystemAudioBackendSDL2}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
Text="{locale:Locale SettingsTabSystemAudioVolume}"
ToolTip.Tip="{locale:Locale AudioVolumeTooltip}"
Width="250" />
<ui:NumberBox Value="{Binding Volume}"
ToolTip.Tip="{locale:Locale AudioVolumeTooltip}"
Width="350"
SmallChange="1"
LargeChange="10"
SimpleNumberFormat="F0"
SpinButtonPlacementMode="Inline"
Minimum="0"
Maximum="100" />
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<Slider Value="{Binding Volume}"
Margin="250,0,0,0"
ToolTip.Tip="{locale:Locale AudioVolumeTooltip}"
Minimum="0"
Maximum="100"
SmallChange="5"
TickFrequency="5"
IsSnapToTickEnabled="True"
LargeChange="10"
Width="350" />
</StackPanel>
</StackPanel>
</Border>
</ScrollViewer>
<ScrollViewer
Name="NetworkPage"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Border Classes="settings">
<StackPanel
Margin="10,5"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabNetworkConnection}" />
<CheckBox Margin="10,0,0,0" IsChecked="{Binding EnableInternetAccess}">
<TextBlock Text="{locale:Locale SettingsTabSystemEnableInternetAccess}"
ToolTip.Tip="{locale:Locale EnableInternetAccessTooltip}" />
</CheckBox>
</StackPanel>
</Border>
</ScrollViewer>
<ScrollViewer
Name="LoggingPage"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Border Classes="settings">
<StackPanel
Margin="10,5"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabLoggingLogging}" />
<StackPanel Margin="10,0,0,0" Orientation="Vertical">
<CheckBox IsChecked="{Binding EnableFileLog}"
ToolTip.Tip="{locale:Locale FileLogTooltip}">
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableLoggingToFile}" />
</CheckBox>
<CheckBox IsChecked="{Binding EnableStub}"
ToolTip.Tip="{locale:Locale StubLogTooltip}">
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableStubLogs}" />
</CheckBox>
<CheckBox IsChecked="{Binding EnableInfo}"
ToolTip.Tip="{locale:Locale InfoLogTooltip}">
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableInfoLogs}" />
</CheckBox>
<CheckBox IsChecked="{Binding EnableWarn}"
ToolTip.Tip="{locale:Locale WarnLogTooltip}">
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableWarningLogs}" />
</CheckBox>
<CheckBox IsChecked="{Binding EnableError}"
ToolTip.Tip="{locale:Locale ErrorLogTooltip}">
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableErrorLogs}" />
</CheckBox>
<CheckBox IsChecked="{Binding EnableTrace}"
ToolTip.Tip="{locale:Locale TraceLogTooltip}">
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableTraceLogs}" />
</CheckBox>
<CheckBox IsChecked="{Binding EnableGuest}"
ToolTip.Tip="{locale:Locale GuestLogTooltip}">
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableGuestLogs}" />
</CheckBox>
</StackPanel>
<Separator Height="1" />
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabLoggingDeveloperOptions}" />
<StackPanel
Margin="10,0,0,0"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
<StackPanel Orientation="Vertical">
<CheckBox IsChecked="{Binding EnableDebug}"
ToolTip.Tip="{locale:Locale DebugLogTooltip}">
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableDebugLogs}" />
</CheckBox>
<CheckBox IsChecked="{Binding EnableFsAccessLog}"
ToolTip.Tip="{locale:Locale FileAccessLogTooltip}">
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableFsAccessLogs}" />
</CheckBox>
<StackPanel Margin="0,10,0,0" Orientation="Horizontal" VerticalAlignment="Stretch">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale FSAccessLogModeTooltip}"
Text="{locale:Locale SettingsTabLoggingFsGlobalAccessLogMode}"
Width="285" />
<ui:NumberBox
Maximum="3"
Minimum="0"
Width="150"
SpinButtonPlacementMode="Inline"
SmallChange="1"
LargeChange="1"
Value="{Binding FsGlobalAccessLogMode}" />
</StackPanel>
<StackPanel Margin="0,10,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
Text="{locale:Locale SettingsTabLoggingGraphicsBackendLogLevel}"
ToolTip.Tip="{locale:Locale OpenGlLogLevel}"
Width="285" />
<ComboBox SelectedIndex="{Binding OpenglDebugLevel}"
Width="150"
HorizontalContentAlignment="Left"
ToolTip.Tip="{locale:Locale OpenGlLogLevel}">
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabLoggingGraphicsBackendLogLevelNone}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabLoggingGraphicsBackendLogLevelError}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock
Text="{locale:Locale SettingsTabLoggingGraphicsBackendLogLevelPerformance}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabLoggingGraphicsBackendLogLevelAll}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
</StackPanel>
</StackPanel>
</StackPanel>
</Border>
</ScrollViewer>
<settings:SettingsUIView Name="UiPage" />
<settings:SettingsInputView Name="InputPage" />
<settings:SettingsHotkeysView Name="HotkeysPage" />
<settings:SettingsSystemView Name="SystemPage" />
<settings:SettingsCPUView Name="CpuPage" />
<settings:SettingsGraphicsView Name="GraphicsPage" />
<settings:SettingsAudioView Name="AudioPage" />
<settings:SettingsNetworkView Name="NetworkPage" />
<settings:SettingsLoggingView Name="LoggingPage" />
</Grid>
<ui:NavigationView Grid.Row="1"
IsSettingsVisible="False"
@ -962,7 +102,7 @@
Spacing="10"
Orientation="Horizontal"
HorizontalAlignment="Right"
ReverseOrder="{ReflectionBinding IsMacOS}">
ReverseOrder="{Binding IsMacOS}">
<Button
HotKey="Enter"
Classes="accent"

View File

@ -1,49 +1,29 @@
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Data.Converters;
using Avalonia.Input;
using Avalonia.Interactivity;
using FluentAvalonia.Core;
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.HLE.FileSystem;
using Ryujinx.Input;
using Ryujinx.Input.Assigner;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using TimeZone = Ryujinx.Ava.UI.Models.TimeZone;
using System.ComponentModel;
namespace Ryujinx.Ava.UI.Windows
{
public partial class SettingsWindow : StyleableWindow
{
private ButtonKeyAssigner _currentAssigner;
internal SettingsViewModel ViewModel { get; set; }
public SettingsWindow(VirtualFileSystem virtualFileSystem, ContentManager contentManager)
{
Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance[LocaleKeys.Settings]}";
ViewModel = new SettingsViewModel(virtualFileSystem, contentManager, this);
ViewModel = new SettingsViewModel(virtualFileSystem, contentManager);
DataContext = ViewModel;
ViewModel.CloseWindow += Close;
ViewModel.SaveSettingsEvent += SaveSettings;
InitializeComponent();
Load();
FuncMultiValueConverter<string, string> converter = new(parts => string.Format("{0} {1} {2}", parts.ToArray()).Trim());
MultiBinding tzMultiBinding = new() { Converter = converter };
tzMultiBinding.Bindings.Add(new Binding("UtcDifference"));
tzMultiBinding.Bindings.Add(new Binding("Location"));
tzMultiBinding.Bindings.Add(new Binding("Abbreviation"));
TimeZoneBox.ValueMemberBinding = tzMultiBinding;
}
public SettingsWindow()
@ -55,6 +35,16 @@ namespace Ryujinx.Ava.UI.Windows
Load();
}
public void SaveSettings()
{
InputPage.ControllerSettings?.SaveCurrentProfile();
if (Owner is MainWindow window && ViewModel.DirectoryChanged)
{
window.ViewModel.LoadApplications();
}
}
private void Load()
{
Pages.Children.Clear();
@ -62,152 +52,52 @@ namespace Ryujinx.Ava.UI.Windows
NavPanel.SelectedItem = NavPanel.MenuItems.ElementAt(0);
}
private void Button_Checked(object sender, RoutedEventArgs e)
{
if (sender is ToggleButton button)
{
if (_currentAssigner != null && button == _currentAssigner.ToggledButton)
{
return;
}
if (_currentAssigner == null && (bool)button.IsChecked)
{
_currentAssigner = new ButtonKeyAssigner(button);
FocusManager.Instance.Focus(this, NavigationMethod.Pointer);
PointerPressed += MouseClick;
IKeyboard keyboard = (IKeyboard)ViewModel.AvaloniaKeyboardDriver.GetGamepad(ViewModel.AvaloniaKeyboardDriver.GamepadsIds[0]);
IButtonAssigner assigner = new KeyboardKeyAssigner(keyboard);
_currentAssigner.GetInputAndAssign(assigner);
}
else
{
if (_currentAssigner != null)
{
ToggleButton oldButton = _currentAssigner.ToggledButton;
_currentAssigner.Cancel();
_currentAssigner = null;
button.IsChecked = false;
}
}
}
}
private void Button_Unchecked(object sender, RoutedEventArgs e)
{
_currentAssigner?.Cancel();
_currentAssigner = null;
}
private void MouseClick(object sender, PointerPressedEventArgs e)
{
bool shouldUnbind = false;
if (e.GetCurrentPoint(this).Properties.IsMiddleButtonPressed)
{
shouldUnbind = true;
}
_currentAssigner?.Cancel(shouldUnbind);
PointerPressed -= MouseClick;
}
private void NavPanelOnSelectionChanged(object sender, NavigationViewSelectionChangedEventArgs e)
{
if (e.SelectedItem is NavigationViewItem navitem)
if (e.SelectedItem is NavigationViewItem navItem && navItem.Tag is not null)
{
NavPanel.Content = navitem.Tag.ToString() switch
switch (navItem.Tag.ToString())
{
"UiPage" => UiPage,
"InputPage" => InputPage,
"HotkeysPage" => HotkeysPage,
"SystemPage" => SystemPage,
"CpuPage" => CpuPage,
"GraphicsPage" => GraphicsPage,
"AudioPage" => AudioPage,
"NetworkPage" => NetworkPage,
"LoggingPage" => LoggingPage,
_ => throw new NotImplementedException()
};
}
}
private async void AddButton_OnClick(object sender, RoutedEventArgs e)
{
string path = PathBox.Text;
if (!string.IsNullOrWhiteSpace(path) && Directory.Exists(path) && !ViewModel.GameDirectories.Contains(path))
{
ViewModel.GameDirectories.Add(path);
ViewModel.DirectoryChanged = true;
}
else
{
path = await new OpenFolderDialog().ShowAsync(this);
if (!string.IsNullOrWhiteSpace(path))
{
ViewModel.GameDirectories.Add(path);
ViewModel.DirectoryChanged = true;
case "UiPage":
UiPage.ViewModel = ViewModel;
NavPanel.Content = UiPage;
break;
case "InputPage":
NavPanel.Content = InputPage;
break;
case "HotkeysPage":
NavPanel.Content = HotkeysPage;
break;
case "SystemPage":
SystemPage.ViewModel = ViewModel;
NavPanel.Content = SystemPage;
break;
case "CpuPage":
NavPanel.Content = CpuPage;
break;
case "GraphicsPage":
NavPanel.Content = GraphicsPage;
break;
case "AudioPage":
NavPanel.Content = AudioPage;
break;
case "NetworkPage":
NavPanel.Content = NetworkPage;
break;
case "LoggingPage":
NavPanel.Content = LoggingPage;
break;
default:
throw new NotImplementedException();
}
}
}
private void RemoveButton_OnClick(object sender, RoutedEventArgs e)
protected override void OnClosing(CancelEventArgs e)
{
int oldIndex = GameList.SelectedIndex;
foreach (string path in new List<string>(GameList.SelectedItems.Cast<string>()))
{
ViewModel.GameDirectories.Remove(path);
ViewModel.DirectoryChanged = true;
}
if (GameList.ItemCount > 0)
{
GameList.SelectedIndex = oldIndex < GameList.ItemCount ? oldIndex : 0;
}
}
private void TimeZoneBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems != null && e.AddedItems.Count > 0)
{
if (e.AddedItems[0] is TimeZone timeZone)
{
e.Handled = true;
ViewModel.ValidateAndSetTimeZone(timeZone.Location);
}
}
}
private void TimeZoneBox_OnTextChanged(object sender, EventArgs e)
{
if (sender is AutoCompleteBox box)
{
if (box.SelectedItem != null && box.SelectedItem is TimeZone timeZone)
{
ViewModel.ValidateAndSetTimeZone(timeZone.Location);
}
}
}
protected override void OnClosed(EventArgs e)
{
ControllerSettings.Dispose();
_currentAssigner?.Cancel();
_currentAssigner = null;
base.OnClosed(e);
HotkeysPage.Dispose();
InputPage.Dispose();
base.OnClosing(e);
}
}
}

View File

@ -7,9 +7,9 @@ namespace Ryujinx.Common.Logging
AudioRenderer,
Configuration,
Cpu,
Font,
Emulation,
FFmpeg,
Font,
Gpu,
Hid,
Host1x,
@ -66,6 +66,7 @@ namespace Ryujinx.Common.Logging
ServiceVi,
SurfaceFlinger,
TamperMachine,
Ui,
Vic
}
}

View File

@ -29,7 +29,7 @@ namespace Ryujinx.Common.Logging
files[i].Delete();
}
string version = ReleaseInformations.GetVersion();
string version = ReleaseInformation.GetVersion();
// Get path for the current time
path = Path.Combine(logDir.FullName, $"Ryujinx_{version}_{DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")}.log");

View File

@ -5,7 +5,7 @@ using System.Reflection;
namespace Ryujinx.Common
{
// DO NOT EDIT, filled by CI
public static class ReleaseInformations
public static class ReleaseInformation
{
private const string FlatHubChannelOwner = "flathub";
@ -50,4 +50,4 @@ namespace Ryujinx.Common
return AppDomain.CurrentDomain.BaseDirectory;
}
}
}
}

View File

@ -35,5 +35,27 @@ namespace Ryujinx.Cpu
/// <param name="address">Address of the region to be invalidated</param>
/// <param name="size">Size of the region to be invalidated</param>
void InvalidateCacheRegion(ulong address, ulong size);
/// <summary>
/// Loads cached code from disk for a given application.
/// </summary>
/// <remarks>
/// If the execution engine is recompiling guest code, this can be used to load cached code from disk.
/// </remarks>
/// <param name="titleIdText">Title ID of the application in padded hex form</param>
/// <param name="displayVersion">Version of the application</param>
/// <param name="enabled">True if the cache should be loaded from disk if it exists, false otherwise</param>
/// <returns>Disk cache load progress reporter and manager</returns>
IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled);
/// <summary>
/// Indicates that code has been loaded into guest memory, and that it might be executed in the future.
/// </summary>
/// <remarks>
/// Some execution engines might use this information to cache recompiled code on disk or to ensure it can be executed.
/// </remarks>
/// <param name="address">CPU virtual address where the code starts</param>
/// <param name="size">Size of the code range in bytes</param>
void PrepareCodeRange(ulong address, ulong size);
}
}

View File

@ -0,0 +1,20 @@
using System;
namespace Ryujinx.Cpu
{
/// <summary>
/// Disk cache load state report and management interface.
/// </summary>
public interface IDiskCacheLoadState
{
/// <summary>
/// Event used to report the cache load progress.
/// </summary>
event Action<LoadState, int, int> StateChanged;
/// <summary>
/// Cancels the disk cache load process.
/// </summary>
void Cancel();
}
}

View File

@ -37,5 +37,17 @@ namespace Ryujinx.Cpu.Jit
{
_translator.InvalidateJitCacheRegion(address, size);
}
/// <inheritdoc/>
public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled)
{
return new JitDiskCacheLoadState(_translator.LoadDiskCache(titleIdText, displayVersion, enabled));
}
/// <inheritdoc/>
public void PrepareCodeRange(ulong address, ulong size)
{
_translator.PrepareCodeRange(address, size);
}
}
}

View File

@ -0,0 +1,38 @@
using ARMeilleure.Translation.PTC;
using System;
namespace Ryujinx.Cpu.Jit
{
public class JitDiskCacheLoadState : IDiskCacheLoadState
{
/// <inheritdoc/>
public event Action<LoadState, int, int> StateChanged;
private readonly IPtcLoadState _loadState;
public JitDiskCacheLoadState(IPtcLoadState loadState)
{
loadState.PtcStateChanged += LoadStateChanged;
_loadState = loadState;
}
private void LoadStateChanged(PtcLoadingState newState, int current, int total)
{
LoadState state = newState switch
{
PtcLoadingState.Start => LoadState.Unloaded,
PtcLoadingState.Loading => LoadState.Loading,
PtcLoadingState.Loaded => LoadState.Loaded,
_ => throw new ArgumentException($"Invalid load state \"{newState}\".")
};
StateChanged?.Invoke(state, current, total);
}
/// <inheritdoc/>
public void Cancel()
{
_loadState.Continue();
}
}
}

12
Ryujinx.Cpu/LoadState.cs Normal file
View File

@ -0,0 +1,12 @@
namespace Ryujinx.Cpu
{
/// <summary>
/// Load state.
/// </summary>
public enum LoadState
{
Unloaded,
Loading,
Loaded
}
}

View File

@ -1,4 +1,3 @@
using ARMeilleure.Translation.PTC;
using LibHac;
using LibHac.Account;
using LibHac.Common;
@ -14,8 +13,8 @@ using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Cpu;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS.Kernel.Process;
using Ryujinx.HLE.Loaders.Executables;
using Ryujinx.Memory;
using System;
@ -67,6 +66,8 @@ namespace Ryujinx.HLE.HOS
public string TitleIdText => TitleId.ToString("x16");
public IDiskCacheLoadState DiskCacheLoadState { get; private set; }
public ApplicationLoader(Switch device)
{
_device = device;
@ -94,7 +95,7 @@ namespace Ryujinx.HLE.HOS
EnsureSaveData(new ApplicationId(TitleId));
}
LoadExeFs(codeFs, metaData);
LoadExeFs(codeFs, string.Empty, metaData);
}
public static (Nca main, Nca patch, Nca control) GetGameData(VirtualFileSystem fileSystem, PartitionFileSystem pfs, int programIndex)
@ -302,12 +303,6 @@ namespace Ryujinx.HLE.HOS
public void LoadServiceNca(string ncaFile)
{
// Disable PPTC here as it does not support multiple processes running.
// TODO: This should be eventually removed and it should stop using global state and
// instead manage the cache per process.
Ptc.Close();
PtcProfiler.Stop();
FileStream file = new FileStream(ncaFile, FileMode.Open, FileAccess.Read);
Nca mainNca = new Nca(_device.Configuration.VirtualFileSystem.KeySet, file.AsStorage(false));
@ -369,16 +364,12 @@ namespace Ryujinx.HLE.HOS
// Collect the nsos, ignoring ones that aren't used.
NsoExecutable[] programs = nsos.Where(x => x != null).ToArray();
MemoryManagerMode memoryManagerMode = _device.Configuration.MemoryManagerMode;
if (!MemoryBlock.SupportsFlags(MemoryAllocationFlags.ViewCompatible))
{
memoryManagerMode = MemoryManagerMode.SoftwarePageTable;
}
string displayVersion = _device.System.ContentManager.GetCurrentFirmwareVersion().VersionString;
bool usePtc = _device.System.EnablePtc;
metaData.GetNpdm(out Npdm npdm).ThrowIfFailure();
ProgramInfo programInfo = new ProgramInfo(in npdm, allowCodeMemoryForJit: false);
ProgramLoader.LoadNsos(_device.System.KernelContext, out _, metaData, programInfo, executables: programs);
ProgramInfo programInfo = new ProgramInfo(in npdm, displayVersion, usePtc, allowCodeMemoryForJit: false);
ProgramLoader.LoadNsos(_device.System.KernelContext, metaData, programInfo, executables: programs);
string titleIdText = npdm.Aci.Value.ProgramId.Value.ToString("x16");
bool titleIs64Bit = (npdm.Meta.Value.Flags & 1) != 0;
@ -477,9 +468,11 @@ namespace Ryujinx.HLE.HOS
_device.Configuration.VirtualFileSystem.ModLoader.GetModsBasePath(),
_device.Configuration.VirtualFileSystem.ModLoader.GetSdModsBasePath());
string displayVersion = string.Empty;
if (controlNca != null)
{
ReadControlData(_device, controlNca, ref _controlData, ref _titleName, ref _displayVersion);
ReadControlData(_device, controlNca, ref _controlData, ref _titleName, ref displayVersion);
}
else
{
@ -493,9 +486,11 @@ namespace Ryujinx.HLE.HOS
string dummyTitleName = "";
BlitStruct<ApplicationControlProperty> dummyControl = new BlitStruct<ApplicationControlProperty>(1);
ReadControlData(_device, updateProgram0ControlNca, ref dummyControl, ref dummyTitleName, ref _displayVersion);
ReadControlData(_device, updateProgram0ControlNca, ref dummyControl, ref dummyTitleName, ref displayVersion);
}
_displayVersion = displayVersion;
if (dataStorage == null)
{
Logger.Warning?.Print(LogClass.Loader, "No RomFS found in NCA");
@ -515,7 +510,7 @@ namespace Ryujinx.HLE.HOS
EnsureSaveData(new ApplicationId(TitleId & ~0xFul));
}
LoadExeFs(codeFs, metaData);
LoadExeFs(codeFs, displayVersion, metaData);
Logger.Info?.Print(LogClass.Loader, $"Application Loaded: {TitleName} v{DisplayVersion} [{TitleIdText}] [{(TitleIs64Bit ? "64-bit" : "32-bit")}]");
}
@ -584,7 +579,7 @@ namespace Ryujinx.HLE.HOS
}
}
private void LoadExeFs(IFileSystem codeFs, MetaLoader metaData = null, bool isHomebrew = false)
private void LoadExeFs(IFileSystem codeFs, string displayVersion, MetaLoader metaData = null, bool isHomebrew = false)
{
if (_device.Configuration.VirtualFileSystem.ModLoader.ReplaceExefsPartition(TitleId, ref codeFs))
{
@ -649,23 +644,23 @@ namespace Ryujinx.HLE.HOS
memoryManagerMode = MemoryManagerMode.SoftwarePageTable;
}
Ptc.Initialize(TitleIdText, DisplayVersion, usePtc, memoryManagerMode);
// We allow it for nx-hbloader because it can be used to launch homebrew.
bool allowCodeMemoryForJit = TitleId == 0x010000000000100DUL || isHomebrew;
metaData.GetNpdm(out Npdm npdm).ThrowIfFailure();
ProgramInfo programInfo = new ProgramInfo(in npdm, allowCodeMemoryForJit);
ProgramLoader.LoadNsos(_device.System.KernelContext, out ProcessTamperInfo tamperInfo, metaData, programInfo, executables: programs);
ProgramInfo programInfo = new ProgramInfo(in npdm, displayVersion, usePtc, allowCodeMemoryForJit);
ProgramLoadResult result = ProgramLoader.LoadNsos(_device.System.KernelContext, metaData, programInfo, executables: programs);
_device.Configuration.VirtualFileSystem.ModLoader.LoadCheats(TitleId, tamperInfo, _device.TamperMachine);
DiskCacheLoadState = result.DiskCacheLoadState;
_device.Configuration.VirtualFileSystem.ModLoader.LoadCheats(TitleId, result.TamperInfo, _device.TamperMachine);
}
public void LoadProgram(string filePath)
{
MetaLoader metaData = GetDefaultNpdm();
metaData.GetNpdm(out Npdm npdm).ThrowIfFailure();
ProgramInfo programInfo = new ProgramInfo(in npdm, allowCodeMemoryForJit: true);
ProgramInfo programInfo = new ProgramInfo(in npdm, string.Empty, diskCacheEnabled: false, allowCodeMemoryForJit: true);
bool isNro = Path.GetExtension(filePath).ToLower() == ".nro";
@ -761,9 +756,11 @@ namespace Ryujinx.HLE.HOS
Graphics.Gpu.GraphicsConfig.TitleId = null;
_device.Gpu.HostInitalized.Set();
ProgramLoader.LoadNsos(_device.System.KernelContext, out ProcessTamperInfo tamperInfo, metaData, programInfo, executables: executable);
ProgramLoadResult result = ProgramLoader.LoadNsos(_device.System.KernelContext, metaData, programInfo, executables: executable);
_device.Configuration.VirtualFileSystem.ModLoader.LoadCheats(TitleId, tamperInfo, _device.TamperMachine);
DiskCacheLoadState = result.DiskCacheLoadState;
_device.Configuration.VirtualFileSystem.ModLoader.LoadCheats(TitleId, result.TamperInfo, _device.TamperMachine);
}
private MetaLoader GetDefaultNpdm()

View File

@ -6,7 +6,17 @@ using Ryujinx.Memory;
namespace Ryujinx.HLE.HOS
{
class ArmProcessContext<T> : IProcessContext where T : class, IVirtualMemoryManagerTracked, IMemoryManager
interface IArmProcessContext : IProcessContext
{
IDiskCacheLoadState Initialize(
string titleIdText,
string displayVersion,
bool diskCacheEnabled,
ulong codeAddress,
ulong codeSize);
}
class ArmProcessContext<T> : IArmProcessContext where T : class, IVirtualMemoryManagerTracked, IMemoryManager
{
private readonly ulong _pid;
private readonly GpuContext _gpuContext;
@ -40,6 +50,17 @@ namespace Ryujinx.HLE.HOS
_cpuContext.Execute(context, codeAddress);
}
public IDiskCacheLoadState Initialize(
string titleIdText,
string displayVersion,
bool diskCacheEnabled,
ulong codeAddress,
ulong codeSize)
{
_cpuContext.PrepareCodeRange(codeAddress, codeSize);
return _cpuContext.LoadDiskCache(titleIdText, displayVersion, diskCacheEnabled);
}
public void InvalidateCacheRegion(ulong address, ulong size)
{
_cpuContext.InvalidateCacheRegion(address, size);

View File

@ -13,11 +13,30 @@ namespace Ryujinx.HLE.HOS
{
private readonly ICpuEngine _cpuEngine;
private readonly GpuContext _gpu;
private readonly string _titleIdText;
private readonly string _displayVersion;
private readonly bool _diskCacheEnabled;
private readonly ulong _codeAddress;
private readonly ulong _codeSize;
public ArmProcessContextFactory(ICpuEngine cpuEngine, GpuContext gpu)
public IDiskCacheLoadState DiskCacheLoadState { get; private set; }
public ArmProcessContextFactory(
ICpuEngine cpuEngine,
GpuContext gpu,
string titleIdText,
string displayVersion,
bool diskCacheEnabled,
ulong codeAddress,
ulong codeSize)
{
_cpuEngine = cpuEngine;
_gpu = gpu;
_titleIdText = titleIdText;
_displayVersion = displayVersion;
_diskCacheEnabled = diskCacheEnabled;
_codeAddress = codeAddress;
_codeSize = codeSize;
}
public IProcessContext Create(KernelContext context, ulong pid, ulong addressSpaceSize, InvalidAccessHandler invalidAccessHandler, bool for64Bit)
@ -29,21 +48,29 @@ namespace Ryujinx.HLE.HOS
mode = MemoryManagerMode.SoftwarePageTable;
}
IArmProcessContext processContext;
switch (mode)
{
case MemoryManagerMode.SoftwarePageTable:
var memoryManager = new MemoryManager(context.Memory, addressSpaceSize, invalidAccessHandler);
return new ArmProcessContext<MemoryManager>(pid, _cpuEngine, _gpu, memoryManager, for64Bit);
processContext = new ArmProcessContext<MemoryManager>(pid, _cpuEngine, _gpu, memoryManager, for64Bit);
break;
case MemoryManagerMode.HostMapped:
case MemoryManagerMode.HostMappedUnsafe:
bool unsafeMode = mode == MemoryManagerMode.HostMappedUnsafe;
var memoryManagerHostMapped = new MemoryManagerHostMapped(context.Memory, addressSpaceSize, unsafeMode, invalidAccessHandler);
return new ArmProcessContext<MemoryManagerHostMapped>(pid, _cpuEngine, _gpu, memoryManagerHostMapped, for64Bit);
processContext = new ArmProcessContext<MemoryManagerHostMapped>(pid, _cpuEngine, _gpu, memoryManagerHostMapped, for64Bit);
break;
default:
throw new ArgumentOutOfRangeException();
}
DiskCacheLoadState = processContext.Initialize(_titleIdText, _displayVersion, _diskCacheEnabled, _codeAddress, _codeSize);
return processContext;
}
}
}

View File

@ -123,6 +123,8 @@ namespace Ryujinx.HLE.HOS
internal LibHacHorizonManager LibHacHorizonManager { get; private set; }
internal ServiceTable ServiceTable { get; private set; }
public bool IsPaused { get; private set; }
public Horizon(Switch device)
@ -326,6 +328,7 @@ namespace Ryujinx.HLE.HOS
private void StartNewServices()
{
ServiceTable = new ServiceTable();
var services = ServiceTable.GetServices(new HorizonOptions(Device.Configuration.IgnoreMissingServices));
foreach (var service in services)

View File

@ -44,7 +44,13 @@ namespace Ryujinx.HLE.HOS.Kernel.Common
public void ScheduleFutureInvocation(IKFutureSchedulerObject schedulerObj, long timeout)
{
long timePoint = PerformanceCounter.ElapsedTicks + ConvertNanosecondsToHostTicks(timeout);
long startTime = PerformanceCounter.ElapsedTicks;
long timePoint = startTime + ConvertNanosecondsToHostTicks(timeout);
if (timePoint < startTime)
{
timePoint = long.MaxValue;
}
lock (_context.CriticalSection.Lock)
{

View File

@ -1,9 +1,9 @@
using ARMeilleure.Translation.PTC;
using LibHac.Loader;
using LibHac.Ncm;
using LibHac.Util;
using Ryujinx.Common;
using Ryujinx.Common.Logging;
using Ryujinx.Cpu;
using Ryujinx.HLE.HOS.Kernel;
using Ryujinx.HLE.HOS.Kernel.Common;
using Ryujinx.HLE.HOS.Kernel.Memory;
@ -21,16 +21,40 @@ namespace Ryujinx.HLE.HOS
{
public string Name;
public ulong ProgramId;
public bool AllowCodeMemoryForJit;
public readonly string TitleIdText;
public readonly string DisplayVersion;
public readonly bool DiskCacheEnabled;
public readonly bool AllowCodeMemoryForJit;
public ProgramInfo(in Npdm npdm, bool allowCodeMemoryForJit)
public ProgramInfo(in Npdm npdm, string displayVersion, bool diskCacheEnabled, bool allowCodeMemoryForJit)
{
ulong programId = npdm.Aci.Value.ProgramId.Value;
Name = StringUtils.Utf8ZToString(npdm.Meta.Value.ProgramName);
ProgramId = npdm.Aci.Value.ProgramId.Value;
ProgramId = programId;
TitleIdText = programId.ToString("x16");
DisplayVersion = displayVersion;
DiskCacheEnabled = diskCacheEnabled;
AllowCodeMemoryForJit = allowCodeMemoryForJit;
}
}
struct ProgramLoadResult
{
public static ProgramLoadResult Failed => new ProgramLoadResult(false, null, null);
public readonly bool Success;
public readonly ProcessTamperInfo TamperInfo;
public readonly IDiskCacheLoadState DiskCacheLoadState;
public ProgramLoadResult(bool success, ProcessTamperInfo tamperInfo, IDiskCacheLoadState diskCacheLoadState)
{
Success = success;
TamperInfo = tamperInfo;
DiskCacheLoadState = diskCacheLoadState;
}
}
static class ProgramLoader
{
private const bool AslrEnabled = true;
@ -102,7 +126,14 @@ namespace Ryujinx.HLE.HOS
KProcess process = new KProcess(context);
var processContextFactory = new ArmProcessContextFactory(context.Device.System.CpuEngine, context.Device.Gpu);
var processContextFactory = new ArmProcessContextFactory(
context.Device.System.CpuEngine,
context.Device.Gpu,
string.Empty,
string.Empty,
false,
codeAddress,
codeSize);
result = process.InitializeKip(
creationInfo,
@ -144,20 +175,20 @@ namespace Ryujinx.HLE.HOS
return true;
}
public static bool LoadNsos(
public static ProgramLoadResult LoadNsos(
KernelContext context,
out ProcessTamperInfo tamperInfo,
MetaLoader metaData,
ProgramInfo programInfo,
byte[] arguments = null,
params IExecutable[] executables)
{
context.Device.System.ServiceTable.WaitServicesReady();
LibHac.Result rc = metaData.GetNpdm(out var npdm);
if (rc.IsFailure())
{
tamperInfo = null;
return false;
return ProgramLoadResult.Failed;
}
ref readonly var meta = ref npdm.Meta.Value;
@ -212,9 +243,6 @@ namespace Ryujinx.HLE.HOS
}
}
PtcProfiler.StaticCodeStart = codeStart;
PtcProfiler.StaticCodeSize = (ulong)codeSize;
int codePagesCount = (int)(codeSize / KPageTableBase.PageSize);
int personalMmHeapPagesCount = (int)(meta.SystemResourceSize / KPageTableBase.PageSize);
@ -263,9 +291,7 @@ namespace Ryujinx.HLE.HOS
{
Logger.Error?.Print(LogClass.Loader, $"Process initialization failed setting resource limit values.");
tamperInfo = null;
return false;
return ProgramLoadResult.Failed;
}
KProcess process = new KProcess(context, programInfo.AllowCodeMemoryForJit);
@ -276,12 +302,17 @@ namespace Ryujinx.HLE.HOS
{
Logger.Error?.Print(LogClass.Loader, $"Process initialization failed due to invalid ACID flags.");
tamperInfo = null;
return false;
return ProgramLoadResult.Failed;
}
var processContextFactory = new ArmProcessContextFactory(context.Device.System.CpuEngine, context.Device.Gpu);
var processContextFactory = new ArmProcessContextFactory(
context.Device.System.CpuEngine,
context.Device.Gpu,
programInfo.TitleIdText,
programInfo.DisplayVersion,
programInfo.DiskCacheEnabled,
codeStart,
codeSize);
result = process.Initialize(
creationInfo,
@ -294,9 +325,7 @@ namespace Ryujinx.HLE.HOS
{
Logger.Error?.Print(LogClass.Loader, $"Process initialization returned error \"{result}\".");
tamperInfo = null;
return false;
return ProgramLoadResult.Failed;
}
for (int index = 0; index < executables.Length; index++)
@ -309,9 +338,7 @@ namespace Ryujinx.HLE.HOS
{
Logger.Error?.Print(LogClass.Loader, $"Process initialization returned error \"{result}\".");
tamperInfo = null;
return false;
return ProgramLoadResult.Failed;
}
}
@ -323,9 +350,7 @@ namespace Ryujinx.HLE.HOS
{
Logger.Error?.Print(LogClass.Loader, $"Process start returned error \"{result}\".");
tamperInfo = null;
return false;
return ProgramLoadResult.Failed;
}
context.Processes.TryAdd(process.Pid, process);
@ -333,10 +358,15 @@ namespace Ryujinx.HLE.HOS
// Keep the build ids because the tamper machine uses them to know which process to associate a
// tamper to and also keep the starting address of each executable inside a process because some
// memory modifications are relative to this address.
tamperInfo = new ProcessTamperInfo(process, buildIds, nsoBase, process.MemoryManager.HeapRegionStart,
process.MemoryManager.AliasRegionStart, process.MemoryManager.CodeRegionStart);
ProcessTamperInfo tamperInfo = new ProcessTamperInfo(
process,
buildIds,
nsoBase,
process.MemoryManager.HeapRegionStart,
process.MemoryManager.AliasRegionStart,
process.MemoryManager.CodeRegionStart);
return true;
return new ProgramLoadResult(true, tamperInfo, processContextFactory.DiskCacheLoadState);
}
private static Result LoadIntoMemory(KProcess process, IExecutable image, ulong baseAddress)

View File

@ -1,19 +0,0 @@
using Ryujinx.HLE.HOS.Services.Lm.LogService;
namespace Ryujinx.HLE.HOS.Services.Lm
{
[Service("lm")]
class ILogService : IpcService
{
public ILogService(ServiceCtx context) { }
[CommandHipc(0)]
// Initialize(u64, pid) -> object<nn::lm::ILogger>
public ResultCode Initialize(ServiceCtx context)
{
MakeObject(context, new ILogger());
return ResultCode.Success;
}
}
}

View File

@ -1,109 +0,0 @@
using Ryujinx.Common.Logging;
using System.IO;
using System.Text;
namespace Ryujinx.HLE.HOS.Services.Lm.LogService
{
class ILogger : IpcService
{
public ILogger() { }
[CommandHipc(0)]
// Log(buffer<unknown, 0x21>)
public ResultCode Log(ServiceCtx context)
{
Logger.Guest?.Print(LogClass.ServiceLm, LogImpl(context));
return ResultCode.Success;
}
private string LogImpl(ServiceCtx context)
{
(ulong bufPos, ulong bufSize) = context.Request.GetBufferType0x21();
byte[] logBuffer = new byte[bufSize];
context.Memory.Read(bufPos, logBuffer);
using MemoryStream ms = new MemoryStream(logBuffer);
BinaryReader reader = new BinaryReader(ms);
long pid = reader.ReadInt64();
long threadContext = reader.ReadInt64();
short flags = reader.ReadInt16();
byte level = reader.ReadByte();
byte verbosity = reader.ReadByte();
int payloadLength = reader.ReadInt32();
StringBuilder sb = new StringBuilder();
sb.AppendLine($"Guest Log:\n Log level: {(LmLogLevel)level}");
while (ms.Position < ms.Length)
{
int type = ReadEncodedInt(reader);
int size = ReadEncodedInt(reader);
LmLogField field = (LmLogField)type;
string fieldStr = string.Empty;
if (field == LmLogField.Start)
{
reader.ReadBytes(size);
continue;
}
else if (field == LmLogField.Stop)
{
break;
}
else if (field == LmLogField.Line)
{
fieldStr = $"{field}: {reader.ReadInt32()}";
}
else if (field == LmLogField.DropCount)
{
fieldStr = $"{field}: {reader.ReadInt64()}";
}
else if (field == LmLogField.Time)
{
fieldStr = $"{field}: {reader.ReadInt64()}s";
}
else if (field < LmLogField.Count)
{
fieldStr = $"{field}: '{Encoding.UTF8.GetString(reader.ReadBytes(size)).TrimEnd()}'";
}
else
{
fieldStr = $"Field{field}: '{Encoding.UTF8.GetString(reader.ReadBytes(size)).TrimEnd()}'";
}
sb.AppendLine($" {fieldStr}");
}
return sb.ToString();
}
private static int ReadEncodedInt(BinaryReader reader)
{
int result = 0;
int position = 0;
byte encoded;
do
{
encoded = reader.ReadByte();
result += (encoded & 0x7F) << (7 * position);
position++;
} while ((encoded & 0x80) != 0);
return result;
}
}
}

View File

@ -1,18 +0,0 @@
namespace Ryujinx.HLE.HOS.Services.Lm.LogService
{
enum LmLogField
{
Start = 0,
Stop = 1,
Message = 2,
Line = 3,
Filename = 4,
Function = 5,
Module = 6,
Thread = 7,
DropCount = 8,
Time = 9,
ProgramName = 10,
Count
}
}

View File

@ -1,11 +0,0 @@
namespace Ryujinx.HLE.HOS.Services.Lm.LogService
{
enum LmLogLevel
{
Trace,
Info,
Warning,
Error,
Critical
}
}

View File

@ -1,182 +0,0 @@
using MsgPack;
using MsgPack.Serialization;
using Ryujinx.Common;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.Utilities;
using System;
using System.Text;
namespace Ryujinx.HLE.HOS.Services.Prepo
{
[Service("prepo:a", PrepoServicePermissionLevel.Admin)] // 1.0.0-5.1.0
[Service("prepo:a2", PrepoServicePermissionLevel.Admin)] // 6.0.0+
[Service("prepo:m", PrepoServicePermissionLevel.Manager)]
[Service("prepo:u", PrepoServicePermissionLevel.User)]
[Service("prepo:s", PrepoServicePermissionLevel.System)]
class IPrepoService : IpcService
{
private PrepoServicePermissionLevel _permission;
private ulong _systemSessionId;
public IPrepoService(ServiceCtx context, PrepoServicePermissionLevel permission)
{
_permission = permission;
}
[CommandHipc(10100)] // 1.0.0-5.1.0
[CommandHipc(10102)] // 6.0.0-9.2.0
[CommandHipc(10104)] // 10.0.0+
// SaveReport(u64, pid, buffer<u8, 9>, buffer<bytes, 5>)
public ResultCode SaveReport(ServiceCtx context)
{
if ((_permission & PrepoServicePermissionLevel.User) == 0)
{
return ResultCode.PermissionDenied;
}
// We don't care about the differences since we don't use the play report.
return ProcessReport(context, withUserID: false);
}
[CommandHipc(10101)] // 1.0.0-5.1.0
[CommandHipc(10103)] // 6.0.0-9.2.0
[CommandHipc(10105)] // 10.0.0+
// SaveReportWithUser(nn::account::Uid, u64, pid, buffer<u8, 9>, buffer<bytes, 5>)
public ResultCode SaveReportWithUser(ServiceCtx context)
{
if ((_permission & PrepoServicePermissionLevel.User) == 0)
{
return ResultCode.PermissionDenied;
}
// We don't care about the differences since we don't use the play report.
return ProcessReport(context, withUserID: true);
}
[CommandHipc(10200)]
// RequestImmediateTransmission()
public ResultCode RequestImmediateTransmission(ServiceCtx context)
{
// It signals an event of nn::prepo::detail::service::core::TransmissionStatusManager that requests the transmission of the report.
// Since we don't use reports it's fine to do nothing.
return ResultCode.Success;
}
[CommandHipc(10300)]
// GetTransmissionStatus() -> u32
public ResultCode GetTransmissionStatus(ServiceCtx context)
{
// It returns the transmission result of nn::prepo::detail::service::core::TransmissionStatusManager.
// Since we don't use reports it's fine to return ResultCode.Success.
context.ResponseData.Write((int)ResultCode.Success);
return ResultCode.Success;
}
[CommandHipc(10400)] // 9.0.0+
// GetSystemSessionId() -> u64
public ResultCode GetSystemSessionId(ServiceCtx context)
{
if ((_permission & PrepoServicePermissionLevel.User) == 0)
{
return ResultCode.PermissionDenied;
}
if (_systemSessionId == 0)
{
byte[] randomBuffer = new byte[8];
Random.Shared.NextBytes(randomBuffer);
_systemSessionId = BitConverter.ToUInt64(randomBuffer, 0);
}
context.ResponseData.Write(_systemSessionId);
return ResultCode.Success;
}
[CommandHipc(20100)]
// SaveSystemReport(u64, pid, buffer<u8, 9>, buffer<bytes, 5>)
public ResultCode SaveSystemReport(ServiceCtx context)
{
if ((_permission & PrepoServicePermissionLevel.System) != 0)
{
return ResultCode.PermissionDenied;
}
// We don't care about the differences since we don't use the play report.
return ProcessReport(context, withUserID: false);
}
[CommandHipc(20101)]
// SaveSystemReportWithUser(nn::account::Uid, u64, pid, buffer<u8, 9>, buffer<bytes, 5>)
public ResultCode SaveSystemReportWithUser(ServiceCtx context)
{
if ((_permission & PrepoServicePermissionLevel.System) != 0)
{
return ResultCode.PermissionDenied;
}
// We don't care about the differences since we don't use the play report.
return ProcessReport(context, withUserID: true);
}
private ResultCode ProcessReport(ServiceCtx context, bool withUserID)
{
UserId userId = withUserID ? context.RequestData.ReadStruct<UserId>() : new UserId();
string gameRoom = StringUtils.ReadUtf8String(context);
if (withUserID)
{
if (userId.IsNull)
{
return ResultCode.InvalidArgument;
}
}
if (gameRoom == string.Empty)
{
return ResultCode.InvalidState;
}
ulong inputPosition = context.Request.SendBuff[0].Position;
ulong inputSize = context.Request.SendBuff[0].Size;
if (inputSize == 0)
{
return ResultCode.InvalidBufferSize;
}
byte[] inputBuffer = new byte[inputSize];
context.Memory.Read(inputPosition, inputBuffer);
Logger.Info?.Print(LogClass.ServicePrepo, ReadReportBuffer(inputBuffer, gameRoom, userId));
return ResultCode.Success;
}
private string ReadReportBuffer(byte[] buffer, string room, UserId userId)
{
StringBuilder builder = new StringBuilder();
MessagePackObject deserializedReport = MessagePackSerializer.UnpackMessagePackObject(buffer);
builder.AppendLine();
builder.AppendLine("PlayReport log:");
if (!userId.IsNull)
{
builder.AppendLine($" UserId: {userId}");
}
builder.AppendLine($" Room: {room}");
builder.AppendLine($" Report: {MessagePackObjectFormatter.Format(deserializedReport)}");
return builder.ToString();
}
}
}

View File

@ -1,15 +0,0 @@
namespace Ryujinx.HLE.HOS.Services.Prepo
{
enum ResultCode
{
ModuleId = 129,
ErrorCodeShift = 9,
Success = 0,
InvalidArgument = (1 << ErrorCodeShift) | ModuleId,
InvalidState = (5 << ErrorCodeShift) | ModuleId,
InvalidBufferSize = (9 << ErrorCodeShift) | ModuleId,
PermissionDenied = (90 << ErrorCodeShift) | ModuleId
}
}

View File

@ -180,7 +180,7 @@ namespace Ryujinx.HLE.HOS.Services.Sm
return ResultCode.InvalidName;
}
Logger.Info?.Print(LogClass.ServiceSm, $"Register \"{name}\".");
Logger.Debug?.Print(LogClass.ServiceSm, $"Register \"{name}\".");
KPort port = new KPort(context.Device.System.KernelContext, maxSessions, isLight, null);

View File

@ -0,0 +1,9 @@
namespace Ryujinx.Headless.SDL2
{
public enum HideCursor
{
Never,
OnIdle,
Always
}
}

View File

@ -5,7 +5,6 @@ using Ryujinx.Common.Logging;
using Ryujinx.Graphics.OpenGL;
using Ryujinx.Input.HLE;
using System;
using static SDL2.SDL;
namespace Ryujinx.Headless.SDL2.OpenGL
@ -103,7 +102,13 @@ namespace Ryujinx.Headless.SDL2.OpenGL
private GraphicsDebugLevel _glLogLevel;
private SDL2OpenGLContext _openGLContext;
public OpenGLWindow(InputManager inputManager, GraphicsDebugLevel glLogLevel, AspectRatio aspectRatio, bool enableMouse) : base(inputManager, glLogLevel, aspectRatio, enableMouse)
public OpenGLWindow(
InputManager inputManager,
GraphicsDebugLevel glLogLevel,
AspectRatio aspectRatio,
bool enableMouse,
HideCursor hideCursor)
: base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursor)
{
_glLogLevel = glLogLevel;
}
@ -161,4 +166,4 @@ namespace Ryujinx.Headless.SDL2.OpenGL
SDL_GL_SwapWindow(WindowHandle);
}
}
}
}

View File

@ -6,6 +6,14 @@ namespace Ryujinx.Headless.SDL2
{
public class Options
{
// General
[Option("root-data-dir", Required = false, HelpText = "Set the custom folder path for Ryujinx data.")]
public string BaseDataDir { get; set; }
[Option("profile", Required = false, HelpText = "Set the user profile to launch the game with.")]
public string UserProfile { get; set; }
// Input
[Option("input-profile-1", Required = false, HelpText = "Set the input profile in use for Player 1.")]
@ -23,7 +31,7 @@ namespace Ryujinx.Headless.SDL2
[Option("input-profile-5", Required = false, HelpText = "Set the input profile in use for Player 5.")]
public string InputProfile5Name { get; set; }
[Option("input-profile-6", Required = false, HelpText = "Set the input profile in use for Player 5.")]
[Option("input-profile-6", Required = false, HelpText = "Set the input profile in use for Player 6.")]
public string InputProfile6Name { get; set; }
[Option("input-profile-7", Required = false, HelpText = "Set the input profile in use for Player 7.")]
@ -63,42 +71,45 @@ namespace Ryujinx.Headless.SDL2
public string InputIdHandheld { get; set; }
[Option("enable-keyboard", Required = false, Default = false, HelpText = "Enable or disable keyboard support (Independent from controllers binding).")]
public bool? EnableKeyboard { get; set; }
public bool EnableKeyboard { get; set; }
[Option("enable-mouse", Required = false, Default = false, HelpText = "Enable or disable mouse support.")]
public bool? EnableMouse { get; set; }
public bool EnableMouse { get; set; }
[Option("hide-cursor", Required = false, Default = HideCursor.OnIdle, HelpText = "Change when the cursor gets hidden.")]
public HideCursor HideCursor { get; set; }
[Option("list-input-profiles", Required = false, HelpText = "List inputs profiles.")]
public bool? ListInputProfiles { get; set; }
public bool ListInputProfiles { get; set; }
[Option("list-inputs-ids", Required = false, HelpText = "List inputs ids.")]
public bool ListInputIds { get; set; }
// System
[Option("enable-ptc", Required = false, Default = true, HelpText = "Enables profiled translation cache persistency.")]
public bool? EnablePtc { get; set; }
[Option("disable-ptc", Required = false, HelpText = "Disables profiled persistent translation cache.")]
public bool DisablePtc { get; set; }
[Option("enable-internet-connection", Required = false, Default = false, HelpText = "Enables guest Internet connection.")]
public bool? EnableInternetAccess { get; set; }
public bool EnableInternetAccess { get; set; }
[Option("enable-fs-integrity-checks", Required = false, Default = true, HelpText = "Enables integrity checks on Game content files.")]
public bool? EnableFsIntegrityChecks { get; set; }
[Option("disable-fs-integrity-checks", Required = false, HelpText = "Disables integrity checks on Game content files.")]
public bool DisableFsIntegrityChecks { get; set; }
[Option("fs-global-access-log-mode", Required = false, Default = 0, HelpText = "Enables FS access log output to the console.")]
public int FsGlobalAccessLogMode { get; set; }
[Option("enable-vsync", Required = false, Default = true, HelpText = "Enables Vertical Sync.")]
public bool? EnableVsync { get; set; }
[Option("disable-vsync", Required = false, HelpText = "Disables Vertical Sync.")]
public bool DisableVsync { get; set; }
[Option("enable-shader-cache", Required = false, Default = true, HelpText = "Enables Shader cache.")]
public bool? EnableShaderCache { get; set; }
[Option("disable-shader-cache", Required = false, HelpText = "Disables Shader cache.")]
public bool DisableShaderCache { get; set; }
[Option("enable-texture-recompression", Required = false, Default = false, HelpText = "Enables Texture recompression.")]
public bool? EnableTextureRecompression { get; set; }
public bool EnableTextureRecompression { get; set; }
[Option("enable-docked-mode", Required = false, Default = true, HelpText = "Enables Docked Mode.")]
public bool? EnableDockedMode { get; set; }
[Option("disable-docked-mode", Required = false, HelpText = "Disables Docked Mode.")]
public bool DisableDockedMode { get; set; }
[Option("system-language", Required = false, Default = SystemLanguage.AmericanEnglish, HelpText = "Change System Language.")]
public SystemLanguage SystemLanguage { get; set; }
@ -120,32 +131,32 @@ namespace Ryujinx.Headless.SDL2
// Logging
[Option("enable-file-logging", Required = false, Default = false, HelpText = "Enables logging to a file on disk.")]
public bool? EnableFileLog { get; set; }
[Option("disable-file-logging", Required = false, Default = false, HelpText = "Disables logging to a file on disk.")]
public bool DisableFileLog { get; set; }
[Option("enable-debug-logs", Required = false, Default = false, HelpText = "Enables printing debug log messages.")]
public bool? LoggingEnableDebug { get; set; }
public bool LoggingEnableDebug { get; set; }
[Option("enable-stub-logs", Required = false, Default = true, HelpText = "Enables printing stub log messages.")]
public bool? LoggingEnableStub { get; set; }
[Option("disable-stub-logs", Required = false, HelpText = "Disables printing stub log messages.")]
public bool LoggingDisableStub { get; set; }
[Option("enable-info-logs", Required = false, Default = true, HelpText = "Enables printing info log messages.")]
public bool? LoggingEnableInfo { get; set; }
[Option("disable-info-logs", Required = false, HelpText = "Disables printing info log messages.")]
public bool LoggingDisableInfo { get; set; }
[Option("enable-warning-logs", Required = false, Default = true, HelpText = "Enables printing warning log messages.")]
public bool? LoggingEnableWarning { get; set; }
[Option("disable-warning-logs", Required = false, HelpText = "Disables printing warning log messages.")]
public bool LoggingDisableWarning { get; set; }
[Option("enable-error-logs", Required = false, Default = true, HelpText = "Enables printing error log messages.")]
public bool? LoggingEnableError { get; set; }
[Option("disable-error-logs", Required = false, HelpText = "Disables printing error log messages.")]
public bool LoggingEnableError { get; set; }
[Option("enable-trace-logs", Required = false, Default = false, HelpText = "Enables printing trace log messages.")]
public bool? LoggingEnableTrace { get; set; }
public bool LoggingEnableTrace { get; set; }
[Option("enable-guest-logs", Required = false, Default = true, HelpText = "Enables printing guest log messages.")]
public bool? LoggingEnableGuest { get; set; }
[Option("disable-guest-logs", Required = false, HelpText = "Disables printing guest log messages.")]
public bool LoggingDisableGuest { get; set; }
[Option("enable-fs-access-logs", Required = false, Default = false, HelpText = "Enables printing FS access log messages.")]
public bool? LoggingEnableFsAccessLog { get; set; }
public bool LoggingEnableFsAccessLog { get; set; }
[Option("graphics-debug-level", Required = false, Default = GraphicsDebugLevel.None, HelpText = "Change Graphics API debug log level.")]
public GraphicsDebugLevel LoggingGraphicsDebugLevel { get; set; }
@ -164,6 +175,9 @@ namespace Ryujinx.Headless.SDL2
[Option("backend-threading", Required = false, Default = BackendThreading.Auto, HelpText = "Whether or not backend threading is enabled. The \"Auto\" setting will determine whether threading should be enabled at runtime.")]
public BackendThreading BackendThreading { get; set; }
[Option("disable-macro-hle", Required= false, HelpText = "Disables high-level emulation of Macro code. Leaving this enabled improves performance but may cause graphical glitches in some games.")]
public bool DisableMacroHLE { get; set; }
[Option("graphics-shaders-dump-path", Required = false, HelpText = "Dumps shaders in this local directory. (Developer only)")]
public string GraphicsShadersDumpPath { get; set; }
@ -176,14 +190,14 @@ namespace Ryujinx.Headless.SDL2
// Hacks
[Option("expand-ram", Required = false, Default = false, HelpText = "Expands the RAM amount on the emulated system from 4GiB to 6GiB.")]
public bool? ExpandRam { get; set; }
public bool ExpandRam { get; set; }
[Option("ignore-missing-services", Required = false, Default = false, HelpText = "Enable ignoring missing services.")]
public bool? IgnoreMissingServices { get; set; }
public bool IgnoreMissingServices { get; set; }
// Values
[Value(0, MetaName = "input", HelpText = "Input to load.", Required = true)]
public string InputPath { get; set; }
}
}
}

View File

@ -1,5 +1,4 @@
using ARMeilleure.Translation;
using ARMeilleure.Translation.PTC;
using CommandLine;
using LibHac.Tools.FsSystem;
using Ryujinx.Audio.Backends.SDL2;
@ -12,6 +11,7 @@ using Ryujinx.Common.Configuration.Hid.Keyboard;
using Ryujinx.Common.Logging;
using Ryujinx.Common.SystemInterop;
using Ryujinx.Common.Utilities;
using Ryujinx.Cpu;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.GAL.Multithreading;
using Ryujinx.Graphics.Gpu;
@ -33,7 +33,6 @@ using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Threading;
using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId;
using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
using Key = Ryujinx.Common.Configuration.Hid.Key;
@ -59,24 +58,10 @@ namespace Ryujinx.Headless.SDL2
static void Main(string[] args)
{
Version = ReleaseInformations.GetVersion();
Version = ReleaseInformation.GetVersion();
Console.Title = $"Ryujinx Console {Version} (Headless SDL2)";
AppDataManager.Initialize(null);
_virtualFileSystem = VirtualFileSystem.CreateInstance();
_libHacHorizonManager = new LibHacHorizonManager();
_libHacHorizonManager.InitializeFsServer(_virtualFileSystem);
_libHacHorizonManager.InitializeArpServer();
_libHacHorizonManager.InitializeBcatServer();
_libHacHorizonManager.InitializeSystemClients();
_contentManager = new ContentManager(_virtualFileSystem);
_accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient);
_userChannelPersistence = new UserChannelPersistence();
if (OperatingSystem.IsMacOS() || OperatingSystem.IsLinux())
{
AutoResetEvent invoked = new AutoResetEvent(false);
@ -97,15 +82,9 @@ namespace Ryujinx.Headless.SDL2
};
}
_inputManager = new InputManager(new SDL2KeyboardDriver(), new SDL2GamepadDriver());
GraphicsConfig.EnableShaderCache = true;
Parser.Default.ParseArguments<Options>(args)
.WithParsed(options => Load(options))
.WithParsed(Load)
.WithNotParsed(errors => errors.Output());
_inputManager.Dispose();
}
private static InputConfig HandlePlayerConfiguration(string inputProfileName, string inputId, PlayerIndex index)
@ -343,6 +322,24 @@ namespace Ryujinx.Headless.SDL2
static void Load(Options option)
{
AppDataManager.Initialize(option.BaseDataDir);
_virtualFileSystem = VirtualFileSystem.CreateInstance();
_libHacHorizonManager = new LibHacHorizonManager();
_libHacHorizonManager.InitializeFsServer(_virtualFileSystem);
_libHacHorizonManager.InitializeArpServer();
_libHacHorizonManager.InitializeBcatServer();
_libHacHorizonManager.InitializeSystemClients();
_contentManager = new ContentManager(_virtualFileSystem);
_accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, option.UserProfile);
_userChannelPersistence = new UserChannelPersistence();
_inputManager = new InputManager(new SDL2KeyboardDriver(), new SDL2GamepadDriver());
GraphicsConfig.EnableShaderCache = true;
IGamepad gamepad;
if (option.ListInputIds)
@ -378,8 +375,8 @@ namespace Ryujinx.Headless.SDL2
}
_inputConfiguration = new List<InputConfig>();
_enableKeyboard = (bool)option.EnableKeyboard;
_enableMouse = (bool)option.EnableMouse;
_enableKeyboard = option.EnableKeyboard;
_enableMouse = option.EnableMouse;
void LoadPlayerConfiguration(string inputProfileName, string inputId, PlayerIndex index)
{
@ -407,30 +404,31 @@ namespace Ryujinx.Headless.SDL2
}
// Setup logging level
Logger.SetEnable(LogLevel.Debug, (bool)option.LoggingEnableDebug);
Logger.SetEnable(LogLevel.Stub, (bool)option.LoggingEnableStub);
Logger.SetEnable(LogLevel.Info, (bool)option.LoggingEnableInfo);
Logger.SetEnable(LogLevel.Warning, (bool)option.LoggingEnableWarning);
Logger.SetEnable(LogLevel.Error, (bool)option.LoggingEnableError);
Logger.SetEnable(LogLevel.Trace, (bool)option.LoggingEnableTrace);
Logger.SetEnable(LogLevel.Guest, (bool)option.LoggingEnableGuest);
Logger.SetEnable(LogLevel.AccessLog, (bool)option.LoggingEnableFsAccessLog);
Logger.SetEnable(LogLevel.Debug, option.LoggingEnableDebug);
Logger.SetEnable(LogLevel.Stub, !option.LoggingDisableStub);
Logger.SetEnable(LogLevel.Info, !option.LoggingDisableInfo);
Logger.SetEnable(LogLevel.Warning, !option.LoggingDisableWarning);
Logger.SetEnable(LogLevel.Error, option.LoggingEnableError);
Logger.SetEnable(LogLevel.Trace, option.LoggingEnableTrace);
Logger.SetEnable(LogLevel.Guest, !option.LoggingDisableGuest);
Logger.SetEnable(LogLevel.AccessLog, option.LoggingEnableFsAccessLog);
if ((bool)option.EnableFileLog)
if (!option.DisableFileLog)
{
Logger.AddTarget(new AsyncLogTargetWrapper(
new FileLogTarget(ReleaseInformations.GetBaseApplicationDirectory(), "file"),
new FileLogTarget(ReleaseInformation.GetBaseApplicationDirectory(), "file"),
1000,
AsyncLogTargetOverflowAction.Block
));
}
// Setup graphics configuration
GraphicsConfig.EnableShaderCache = (bool)option.EnableShaderCache;
GraphicsConfig.EnableTextureRecompression = (bool)option.EnableTextureRecompression;
GraphicsConfig.EnableShaderCache = !option.DisableShaderCache;
GraphicsConfig.EnableTextureRecompression = option.EnableTextureRecompression;
GraphicsConfig.ResScale = option.ResScale;
GraphicsConfig.MaxAnisotropy = option.MaxAnisotropy;
GraphicsConfig.ShadersDumpPath = option.GraphicsShadersDumpPath;
GraphicsConfig.EnableMacroHLE = !option.DisableMacroHLE;
while (true)
{
@ -443,12 +441,17 @@ namespace Ryujinx.Headless.SDL2
_userChannelPersistence.ShouldRestart = false;
}
_inputManager.Dispose();
}
private static void SetupProgressHandler()
{
Ptc.PtcStateChanged -= ProgressHandler;
Ptc.PtcStateChanged += ProgressHandler;
if (_emulationContext.Application.DiskCacheLoadState != null)
{
_emulationContext.Application.DiskCacheLoadState.StateChanged -= ProgressHandler;
_emulationContext.Application.DiskCacheLoadState.StateChanged += ProgressHandler;
}
_emulationContext.Gpu.ShaderCacheStateChanged -= ProgressHandler;
_emulationContext.Gpu.ShaderCacheStateChanged += ProgressHandler;
@ -460,7 +463,7 @@ namespace Ryujinx.Headless.SDL2
switch (state)
{
case PtcLoadingState ptcState:
case LoadState ptcState:
label = $"PTC : {current}/{total}";
break;
case ShaderCacheState shaderCacheState:
@ -476,8 +479,8 @@ namespace Ryujinx.Headless.SDL2
private static WindowBase CreateWindow(Options options)
{
return options.GraphicsBackend == GraphicsBackend.Vulkan
? new VulkanWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, (bool)options.EnableMouse)
: new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, (bool)options.EnableMouse);
? new VulkanWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursor)
: new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursor);
}
private static IRenderer CreateRenderer(Options options, WindowBase window)
@ -530,20 +533,20 @@ namespace Ryujinx.Headless.SDL2
_userChannelPersistence,
renderer,
new SDL2HardwareDeviceDriver(),
(bool)options.ExpandRam ? MemoryConfiguration.MemoryConfiguration6GiB : MemoryConfiguration.MemoryConfiguration4GiB,
options.ExpandRam ? MemoryConfiguration.MemoryConfiguration6GiB : MemoryConfiguration.MemoryConfiguration4GiB,
window,
options.SystemLanguage,
options.SystemRegion,
(bool)options.EnableVsync,
(bool)options.EnableDockedMode,
(bool)options.EnablePtc,
(bool)options.EnableInternetAccess,
(bool)options.EnableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None,
!options.DisableVsync,
!options.DisableDockedMode,
!options.DisablePtc,
options.EnableInternetAccess,
!options.DisableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None,
options.FsGlobalAccessLogMode,
options.SystemTimeOffset,
options.SystemTimeZone,
options.MemoryManagerMode,
(bool)options.IgnoreMissingServices,
options.IgnoreMissingServices,
options.AspectRatio,
options.AudioVolume);
@ -563,9 +566,6 @@ namespace Ryujinx.Headless.SDL2
_window.Execute();
Ptc.Close();
PtcProfiler.Stop();
_emulationContext.Dispose();
_window.Dispose();
@ -649,7 +649,7 @@ namespace Ryujinx.Headless.SDL2
}
else
{
Logger.Warning?.Print(LogClass.Application, "Please specify a valid XCI/NCA/NSP/PFS0/NRO file.");
Logger.Warning?.Print(LogClass.Application, $"Couldn't load '{options.InputPath}'. Please specify a valid XCI/NCA/NSP/PFS0/NRO file.");
_emulationContext.Dispose();

View File

@ -42,6 +42,10 @@
</Content>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Ryujinx.bmp" />
</ItemGroup>
<!-- Due to .net core 3.1 embedded resource loading -->
<PropertyGroup>
<EmbeddedResourceUseDependentUponConvention>false</EmbeddedResourceUseDependentUponConvention>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@ -10,7 +10,12 @@ namespace Ryujinx.Headless.SDL2
{
class SDL2MouseDriver : IGamepadDriver
{
private const int CursorHideIdleTime = 8; // seconds
private bool _isDisposed;
private HideCursor _hideCursor;
private bool _isHidden;
private long _lastCursorMoveTime;
public bool[] PressedButtons { get; }
@ -18,9 +23,16 @@ namespace Ryujinx.Headless.SDL2
public Vector2 Scroll { get; private set; }
public Size _clientSize;
public SDL2MouseDriver()
public SDL2MouseDriver(HideCursor hideCursor)
{
PressedButtons = new bool[(int)MouseButton.Count];
_hideCursor = hideCursor;
if (_hideCursor == HideCursor.Always)
{
SDL_ShowCursor(SDL_DISABLE);
_isHidden = true;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -31,26 +43,75 @@ namespace Ryujinx.Headless.SDL2
return (MouseButton)(rawButton - 1);
}
public void Update(SDL_Event evnt)
public void UpdatePosition()
{
if (evnt.type == SDL_EventType.SDL_MOUSEBUTTONDOWN || evnt.type == SDL_EventType.SDL_MOUSEBUTTONUP)
SDL_GetMouseState(out int posX, out int posY);
Vector2 position = new(posX, posY);
if (CurrentPosition != position)
{
uint rawButton = evnt.button.button;
CurrentPosition = position;
_lastCursorMoveTime = Stopwatch.GetTimestamp();
}
if (rawButton > 0 && rawButton <= (int)MouseButton.Count)
CheckIdle();
}
private void CheckIdle()
{
if (_hideCursor != HideCursor.OnIdle)
{
return;
}
long cursorMoveDelta = Stopwatch.GetTimestamp() - _lastCursorMoveTime;
if (cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency)
{
if (!_isHidden)
{
PressedButtons[(int)DriverButtonToMouseButton(rawButton)] = evnt.type == SDL_EventType.SDL_MOUSEBUTTONDOWN;
CurrentPosition = new Vector2(evnt.button.x, evnt.button.y);
SDL_ShowCursor(SDL_DISABLE);
_isHidden = true;
}
}
else if (evnt.type == SDL_EventType.SDL_MOUSEMOTION)
else
{
CurrentPosition = new Vector2(evnt.motion.x, evnt.motion.y);
if (_isHidden)
{
SDL_ShowCursor(SDL_ENABLE);
_isHidden = false;
}
}
else if (evnt.type == SDL_EventType.SDL_MOUSEWHEEL)
}
public void Update(SDL_Event evnt)
{
switch (evnt.type)
{
Scroll = new Vector2(evnt.wheel.x, evnt.wheel.y);
case SDL_EventType.SDL_MOUSEBUTTONDOWN:
case SDL_EventType.SDL_MOUSEBUTTONUP:
uint rawButton = evnt.button.button;
if (rawButton > 0 && rawButton <= (int)MouseButton.Count)
{
PressedButtons[(int)DriverButtonToMouseButton(rawButton)] = evnt.type == SDL_EventType.SDL_MOUSEBUTTONDOWN;
CurrentPosition = new Vector2(evnt.button.x, evnt.button.y);
}
break;
// NOTE: On Linux using Wayland mouse motion events won't be received at all.
case SDL_EventType.SDL_MOUSEMOTION:
CurrentPosition = new Vector2(evnt.motion.x, evnt.motion.y);
_lastCursorMoveTime = Stopwatch.GetTimestamp();
break;
case SDL_EventType.SDL_MOUSEWHEEL:
Scroll = new Vector2(evnt.wheel.x, evnt.wheel.y);
break;
}
}
@ -100,4 +161,4 @@ namespace Ryujinx.Headless.SDL2
_isDisposed = true;
}
}
}
}

View File

@ -12,7 +12,13 @@ namespace Ryujinx.Headless.SDL2.Vulkan
{
private GraphicsDebugLevel _glLogLevel;
public VulkanWindow(InputManager inputManager, GraphicsDebugLevel glLogLevel, AspectRatio aspectRatio, bool enableMouse) : base(inputManager, glLogLevel, aspectRatio, enableMouse)
public VulkanWindow(
InputManager inputManager,
GraphicsDebugLevel glLogLevel,
AspectRatio aspectRatio,
bool enableMouse,
HideCursor hideCursor)
: base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursor)
{
_glLogLevel = glLogLevel;
}
@ -95,4 +101,4 @@ namespace Ryujinx.Headless.SDL2.Vulkan
protected override void SwapBuffers() { }
}
}
}

View File

@ -14,13 +14,16 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using static SDL2.SDL;
using Switch = Ryujinx.HLE.Switch;
namespace Ryujinx.Headless.SDL2
{
abstract class WindowBase : IHostUiHandler, IDisposable
abstract partial class WindowBase : IHostUiHandler, IDisposable
{
protected const int DefaultWidth = 1280;
protected const int DefaultHeight = 720;
@ -29,6 +32,10 @@ namespace Ryujinx.Headless.SDL2
private static ConcurrentQueue<Action> MainThreadActions = new ConcurrentQueue<Action>();
[LibraryImport("SDL2")]
// TODO: Remove this as soon as SDL2-CS was updated to expose this method publicly
private static partial IntPtr SDL_LoadBMP_RW(IntPtr src, int freesrc);
public static void QueueMainThreadAction(Action action)
{
MainThreadActions.Enqueue(action);
@ -66,9 +73,14 @@ namespace Ryujinx.Headless.SDL2
private AspectRatio _aspectRatio;
private bool _enableMouse;
public WindowBase(InputManager inputManager, GraphicsDebugLevel glLogLevel, AspectRatio aspectRatio, bool enableMouse)
public WindowBase(
InputManager inputManager,
GraphicsDebugLevel glLogLevel,
AspectRatio aspectRatio,
bool enableMouse,
HideCursor hideCursor)
{
MouseDriver = new SDL2MouseDriver();
MouseDriver = new SDL2MouseDriver(hideCursor);
_inputManager = inputManager;
_inputManager.SetMouseDriver(MouseDriver);
NpadManager = _inputManager.CreateNpadManager();
@ -103,6 +115,34 @@ namespace Ryujinx.Headless.SDL2
TouchScreenManager.Initialize(device);
}
private void SetWindowIcon()
{
Stream iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Ryujinx.Headless.SDL2.Ryujinx.bmp");
byte[] iconBytes = new byte[iconStream!.Length];
if (iconStream.Read(iconBytes, 0, iconBytes.Length) != iconBytes.Length)
{
Logger.Error?.Print(LogClass.Application, "Failed to read icon to byte array.");
iconStream.Close();
return;
}
iconStream.Close();
unsafe
{
fixed (byte* iconPtr = iconBytes)
{
IntPtr rwOpsStruct = SDL_RWFromConstMem((IntPtr)iconPtr, iconBytes.Length);
IntPtr iconHandle = SDL_LoadBMP_RW(rwOpsStruct, 1);
SDL_SetWindowIcon(WindowHandle, iconHandle);
SDL_FreeSurface(iconHandle);
}
}
}
private void InitializeWindow()
{
string titleNameSection = string.IsNullOrWhiteSpace(Device.Application.TitleName) ? string.Empty
@ -127,6 +167,8 @@ namespace Ryujinx.Headless.SDL2
throw new Exception(errorMessage);
}
SetWindowIcon();
_windowId = SDL_GetWindowID(WindowHandle);
SDL2Driver.Instance.RegisterWindow(_windowId, HandleWindowEvent);
@ -146,9 +188,11 @@ namespace Ryujinx.Headless.SDL2
Renderer?.Window.SetSize(Width, Height);
MouseDriver.SetClientSize(Width, Height);
break;
case SDL_WindowEventID.SDL_WINDOWEVENT_CLOSE:
Exit();
break;
default:
break;
}
@ -331,6 +375,9 @@ namespace Ryujinx.Headless.SDL2
Device.Hid.DebugPad.Update();
// TODO: Replace this with MouseDriver.CheckIdle() when mouse motion events are received on every supported platform.
MouseDriver.UpdatePosition();
return true;
}
@ -451,4 +498,4 @@ namespace Ryujinx.Headless.SDL2
}
}
}
}
}

View File

@ -115,4 +115,4 @@ namespace Ryujinx.Horizon.Common
return PrintableResult;
}
}
}
}

View File

@ -123,44 +123,51 @@ namespace Ryujinx.Horizon.Generators.Hipc
{
string[] args = new string[method.ParameterList.Parameters.Count];
int index = 0;
foreach (var parameter in method.ParameterList.Parameters)
if (args.Length == 0)
{
string canonicalTypeName = GetCanonicalTypeNameWithGenericArguments(compilation, parameter.Type);
CommandArgType argType = GetCommandArgType(compilation, parameter);
generator.AppendLine($"{{ {commandId}, new CommandHandler({method.Identifier.Text}, Array.Empty<CommandArg>()) }},");
}
else
{
int index = 0;
string arg;
if (argType == CommandArgType.Buffer)
foreach (var parameter in method.ParameterList.Parameters)
{
string bufferFlags = GetFirstAttributeAgument(compilation, parameter, TypeBufferAttribute, 0);
string bufferFixedSize = GetFirstAttributeAgument(compilation, parameter, TypeBufferAttribute, 1);
string canonicalTypeName = GetCanonicalTypeNameWithGenericArguments(compilation, parameter.Type);
CommandArgType argType = GetCommandArgType(compilation, parameter);
if (bufferFixedSize != null)
string arg;
if (argType == CommandArgType.Buffer)
{
arg = $"new CommandArg({bufferFlags}, {bufferFixedSize})";
string bufferFlags = GetFirstAttributeAgument(compilation, parameter, TypeBufferAttribute, 0);
string bufferFixedSize = GetFirstAttributeAgument(compilation, parameter, TypeBufferAttribute, 1);
if (bufferFixedSize != null)
{
arg = $"new CommandArg({bufferFlags}, {bufferFixedSize})";
}
else
{
arg = $"new CommandArg({bufferFlags})";
}
}
else if (argType == CommandArgType.InArgument || argType == CommandArgType.OutArgument)
{
string alignment = GetTypeAlignmentExpression(compilation, parameter.Type);
arg = $"new CommandArg(CommandArgType.{argType}, Unsafe.SizeOf<{canonicalTypeName}>(), {alignment})";
}
else
{
arg = $"new CommandArg({bufferFlags})";
arg = $"new CommandArg(CommandArgType.{argType})";
}
}
else if (argType == CommandArgType.InArgument || argType == CommandArgType.OutArgument)
{
string alignment = GetTypeAlignmentExpression(compilation, parameter.Type);
arg = $"new CommandArg(CommandArgType.{argType}, Unsafe.SizeOf<{canonicalTypeName}>(), {alignment})";
}
else
{
arg = $"new CommandArg(CommandArgType.{argType})";
args[index++] = arg;
}
args[index++] = arg;
generator.AppendLine($"{{ {commandId}, new CommandHandler({method.Identifier.Text}, {string.Join(", ", args)}) }},");
}
generator.AppendLine($"{{ {commandId}, new CommandHandler({method.Identifier.Text}, {string.Join(", ", args)}) }},");
}
}

View File

@ -12,12 +12,12 @@ namespace Ryujinx.Horizon
private struct Range : IComparable<Range>
{
public ulong Offset { get; }
public ulong Size { get; }
public ulong Size { get; }
public Range(ulong offset, ulong size)
{
Offset = offset;
Size = size;
Size = size;
}
public int CompareTo(Range other)
@ -31,7 +31,7 @@ namespace Ryujinx.Horizon
public HeapAllocator()
{
_freeRanges = new List<Range>();
_freeRanges = new List<Range>();
_currentHeapSize = 0;
}
@ -70,8 +70,8 @@ namespace Ryujinx.Horizon
var range = _freeRanges[i];
ulong alignedOffset = BitUtils.AlignUp(range.Offset, alignment);
ulong sizeDelta = alignedOffset - range.Offset;
ulong usableSize = range.Size - sizeDelta;
ulong sizeDelta = alignedOffset - range.Offset;
ulong usableSize = range.Size - sizeDelta;
if (sizeDelta < range.Size && usableSize >= size)
{
@ -82,7 +82,7 @@ namespace Ryujinx.Horizon
InsertFreeRange(range.Offset, sizeDelta);
}
ulong endOffset = range.Offset + range.Size;
ulong endOffset = range.Offset + range.Size;
ulong remainingSize = endOffset - (alignedOffset + size);
if (remainingSize != 0)
{

View File

@ -2,11 +2,13 @@ namespace Ryujinx.Horizon
{
public struct HorizonOptions
{
public bool IgnoreMissingServices { get; }
public bool IgnoreMissingServices { get; }
public bool ThrowOnInvalidCommandIds { get; }
public HorizonOptions(bool ignoreMissingServices)
{
IgnoreMissingServices = ignoreMissingServices;
IgnoreMissingServices = ignoreMissingServices;
ThrowOnInvalidCommandIds = true;
}
}
}

View File

@ -21,24 +21,24 @@ namespace Ryujinx.Horizon
[ThreadStatic]
private static int _threadHandle;
public static HorizonOptions Options => _options;
public static ISyscallApi Syscall => _syscall;
public static IVirtualMemoryManager AddressSpace => _addressSpace;
public static IThreadContext ThreadContext => _threadContext;
public static int CurrentThreadHandle => _threadHandle;
public static HorizonOptions Options => _options;
public static ISyscallApi Syscall => _syscall;
public static IVirtualMemoryManager AddressSpace => _addressSpace;
public static IThreadContext ThreadContext => _threadContext;
public static int CurrentThreadHandle => _threadHandle;
public static void Register(
HorizonOptions options,
ISyscallApi syscallApi,
HorizonOptions options,
ISyscallApi syscallApi,
IVirtualMemoryManager addressSpace,
IThreadContext threadContext,
int threadHandle)
IThreadContext threadContext,
int threadHandle)
{
_options = options;
_syscall = syscallApi;
_addressSpace = addressSpace;
_options = options;
_syscall = syscallApi;
_addressSpace = addressSpace;
_threadContext = threadContext;
_threadHandle = threadHandle;
_threadHandle = threadHandle;
}
}
}

View File

@ -2,6 +2,6 @@
{
interface IService
{
abstract static void Main();
abstract static void Main(ServiceTable serviceTable);
}
}

View File

@ -9,23 +9,23 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
namespace Ryujinx.Horizon.LogManager
namespace Ryujinx.Horizon.LogManager.Ipc
{
partial class LmLogger : IServiceObject
partial class LmLogger : ILmLogger
{
private readonly LmLog _log;
private readonly ulong _clientProcessId;
private readonly LogService _log;
private readonly ulong _pid;
public LmLogger(LmLog log, ulong clientProcessId)
public LmLogger(LogService log, ulong pid)
{
_log = log;
_clientProcessId = clientProcessId;
_pid = pid;
}
[CmifCommand(0)]
public Result Log([Buffer(HipcBufferFlags.In | HipcBufferFlags.AutoSelect)] Span<byte> message)
{
if (!SetProcessId(message, _clientProcessId))
if (!SetProcessId(message, _pid))
{
return Result.Success;
}
@ -35,7 +35,7 @@ namespace Ryujinx.Horizon.LogManager
return Result.Success;
}
[CmifCommand(1)]
[CmifCommand(1)] // 3.0.0+
public Result SetDestination(LogDestination destination)
{
_log.LogDestination = destination;
@ -48,7 +48,6 @@ namespace Ryujinx.Horizon.LogManager
ref LogPacketHeader header = ref MemoryMarshal.Cast<byte, LogPacketHeader>(message)[0];
uint expectedMessageSize = (uint)Unsafe.SizeOf<LogPacketHeader>() + header.PayloadSize;
if (expectedMessageSize != (uint)message.Length)
{
Logger.Warning?.Print(LogClass.ServiceLm, $"Invalid message size (expected 0x{expectedMessageSize:X} but got 0x{message.Length:X}).");
@ -63,13 +62,11 @@ namespace Ryujinx.Horizon.LogManager
private static string LogImpl(ReadOnlySpan<byte> message)
{
SpanReader reader = new SpanReader(message);
SpanReader reader = new(message);
LogPacketHeader header = reader.Read<LogPacketHeader>();
StringBuilder builder = new();
LogPacketHeader header = reader.Read<LogPacketHeader>();
StringBuilder sb = new StringBuilder();
sb.AppendLine($"Guest Log:\n Log level: {header.Severity}");
builder.AppendLine($"Guest Log:\n Log level: {header.Severity}");
while (reader.Length > 0)
{
@ -78,7 +75,7 @@ namespace Ryujinx.Horizon.LogManager
LogDataChunkKey field = (LogDataChunkKey)type;
string fieldStr = string.Empty;
string fieldStr;
if (field == LogDataChunkKey.Start)
{
@ -111,16 +108,16 @@ namespace Ryujinx.Horizon.LogManager
fieldStr = $"Field{field}: '{Encoding.UTF8.GetString(reader.GetSpan(size)).TrimEnd()}'";
}
sb.AppendLine($" {fieldStr}");
builder.AppendLine($" {fieldStr}");
}
return sb.ToString();
return builder.ToString();
}
private static int ReadUleb128(ref SpanReader reader)
{
int result = 0;
int count = 0;
int count = 0;
byte encoded;
@ -136,4 +133,4 @@ namespace Ryujinx.Horizon.LogManager
return result;
}
}
}
}

View File

@ -2,18 +2,19 @@
using Ryujinx.Horizon.Sdk.Lm;
using Ryujinx.Horizon.Sdk.Sf;
namespace Ryujinx.Horizon.LogManager
namespace Ryujinx.Horizon.LogManager.Ipc
{
partial class LmLog : IServiceObject
partial class LogService : ILogService
{
public LogDestination LogDestination { get; set; } = LogDestination.TargetManager;
[CmifCommand(0)]
public Result OpenLogger(out LmLogger logger, [ClientProcessId] ulong clientProcessId)
public Result OpenLogger(out LmLogger logger, [ClientProcessId] ulong pid)
{
logger = new LmLogger(this, clientProcessId);
// NOTE: Internal name is Logger, but we rename it LmLogger to avoid name clash with Ryujinx.Common.Logging logger.
logger = new LmLogger(this, pid);
return Result.Success;
}
}
}
}

View File

@ -1,6 +1,6 @@
using Ryujinx.Horizon.Sdk.Sf.Hipc;
using Ryujinx.Horizon.LogManager.Ipc;
using Ryujinx.Horizon.Sdk.Sf.Hipc;
using Ryujinx.Horizon.Sdk.Sm;
using Ryujinx.Horizon.Sm;
namespace Ryujinx.Horizon.LogManager
{
@ -9,36 +9,25 @@ namespace Ryujinx.Horizon.LogManager
private const int LogMaxSessionsCount = 42;
private const int PointerBufferSize = 0x400;
private const int MaxDomains = 31;
private const int MaxDomainObjects = 61;
private const int MaxDomains = 31;
private const int MaxDomainObjects = 61;
private const int MaxPortsCount = 1;
private const int MaxPortsCount = 1;
private static readonly ManagerOptions _logManagerOptions = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false);
private static readonly ManagerOptions _logManagerOptions = new ManagerOptions(
PointerBufferSize,
MaxDomains,
MaxDomainObjects,
false);
private static readonly ServiceName _logServiceName = ServiceName.Encode("lm");
private SmApi _sm;
private SmApi _sm;
private ServerManager _serverManager;
private LmLog _logServiceObject;
public void Initialize()
{
HeapAllocator allocator = new HeapAllocator();
HeapAllocator allocator = new();
_sm = new SmApi();
_sm.Initialize().AbortOnFailure();
_serverManager = new ServerManager(allocator, _sm, MaxPortsCount, _logManagerOptions, LogMaxSessionsCount);
_logServiceObject = new LmLog();
_serverManager.RegisterObjectForServer(_logServiceObject, _logServiceName, LogMaxSessionsCount);
_serverManager.RegisterObjectForServer(new LogService(), ServiceName.Encode("lm"), LogMaxSessionsCount);
}
public void ServiceRequests()
@ -51,4 +40,4 @@ namespace Ryujinx.Horizon.LogManager
_serverManager.Dispose();
}
}
}
}

View File

@ -2,13 +2,16 @@
{
class LmMain : IService
{
public static void Main()
public static void Main(ServiceTable serviceTable)
{
LmIpcServer ipcServer = new LmIpcServer();
LmIpcServer ipcServer = new();
ipcServer.Initialize();
serviceTable.SignalServiceReady();
ipcServer.ServiceRequests();
ipcServer.Shutdown();
}
}
}
}

View File

@ -0,0 +1,218 @@
using MsgPack;
using MsgPack.Serialization;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.Horizon.Common;
using Ryujinx.Horizon.Prepo.Types;
using Ryujinx.Horizon.Sdk.Account;
using Ryujinx.Horizon.Sdk.Prepo;
using Ryujinx.Horizon.Sdk.Sf;
using Ryujinx.Horizon.Sdk.Sf.Hipc;
using System;
using System.Text;
namespace Ryujinx.Horizon.Prepo.Ipc
{
partial class PrepoService : IPrepoService
{
enum PlayReportKind
{
Normal,
System
}
private readonly PrepoServicePermissionLevel _permissionLevel;
private ulong _systemSessionId;
private bool _immediateTransmissionEnabled = false;
private bool _userAgreementCheckEnabled = true;
public PrepoService(PrepoServicePermissionLevel permissionLevel)
{
_permissionLevel = permissionLevel;
}
[CmifCommand(10100)] // 1.0.0-5.1.0
[CmifCommand(10102)] // 6.0.0-9.2.0
[CmifCommand(10104)] // 10.0.0+
public Result SaveReport([Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<byte> gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> reportBuffer, [ClientProcessId] ulong pid)
{
if ((_permissionLevel & PrepoServicePermissionLevel.User) == 0)
{
return PrepoResult.PermissionDenied;
}
ProcessPlayReport(PlayReportKind.Normal, pid, gameRoomBuffer, reportBuffer, Uid.Null);
return Result.Success;
}
[CmifCommand(10101)] // 1.0.0-5.1.0
[CmifCommand(10103)] // 6.0.0-9.2.0
[CmifCommand(10105)] // 10.0.0+
public Result SaveReportWithUser(Uid userId, [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<byte> gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> reportBuffer, [ClientProcessId] ulong pid)
{
if ((_permissionLevel & PrepoServicePermissionLevel.User) == 0)
{
return PrepoResult.PermissionDenied;
}
ProcessPlayReport(PlayReportKind.Normal, pid, gameRoomBuffer, reportBuffer, userId, true);
return Result.Success;
}
[CmifCommand(10200)]
public Result RequestImmediateTransmission()
{
_immediateTransmissionEnabled = true;
// It signals an event of nn::prepo::detail::service::core::TransmissionStatusManager that requests the transmission of the report.
// Since we don't use reports, it's fine to do nothing.
return Result.Success;
}
[CmifCommand(10300)]
public Result GetTransmissionStatus(out int status)
{
status = 0;
if (_immediateTransmissionEnabled && _userAgreementCheckEnabled)
{
status = 1;
}
return Result.Success;
}
[CmifCommand(10400)] // 9.0.0+
public Result GetSystemSessionId(out ulong systemSessionId)
{
systemSessionId = default;
if ((_permissionLevel & PrepoServicePermissionLevel.User) == 0)
{
return PrepoResult.PermissionDenied;
}
if (_systemSessionId == 0)
{
_systemSessionId = (ulong)Random.Shared.NextInt64();
}
systemSessionId = _systemSessionId;
return Result.Success;
}
[CmifCommand(20100)]
public Result SaveSystemReport([Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<byte> gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> reportBuffer, [ClientProcessId] ulong pid)
{
if ((_permissionLevel & PrepoServicePermissionLevel.System) != 0)
{
return PrepoResult.PermissionDenied;
}
return ProcessPlayReport(PlayReportKind.System, pid, gameRoomBuffer, reportBuffer, Uid.Null);
}
[CmifCommand(20101)]
public Result SaveSystemReportWithUser(Uid userId, [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<byte> gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> reportBuffer, [ClientProcessId] ulong pid)
{
if ((_permissionLevel & PrepoServicePermissionLevel.System) != 0)
{
return PrepoResult.PermissionDenied;
}
return ProcessPlayReport(PlayReportKind.System, pid, gameRoomBuffer, reportBuffer, userId, true);
}
[CmifCommand(40100)] // 2.0.0+
public Result IsUserAgreementCheckEnabled(out bool enabled)
{
enabled = false;
if (_permissionLevel == PrepoServicePermissionLevel.User || _permissionLevel == PrepoServicePermissionLevel.System)
{
enabled = _userAgreementCheckEnabled;
// If "enabled" is false, it sets some internal fields to 0.
// Then, it mounts "prepo-sys:/is_user_agreement_check_enabled.bin" and returns the contained bool.
// We can return the private bool instead, we don't care about the agreement since we don't send reports.
return Result.Success;
}
return PrepoResult.PermissionDenied;
}
[CmifCommand(40101)] // 2.0.0+
public Result SetUserAgreementCheckEnabled(bool enabled)
{
if (_permissionLevel == PrepoServicePermissionLevel.User || _permissionLevel == PrepoServicePermissionLevel.System)
{
_userAgreementCheckEnabled = enabled;
// If "enabled" is false, it sets some internal fields to 0.
// Then, it mounts "prepo-sys:/is_user_agreement_check_enabled.bin" and stores the "enabled" value.
// We can store in the private bool instead, we don't care about the agreement since we don't send reports.
return Result.Success;
}
return PrepoResult.PermissionDenied;
}
private static Result ProcessPlayReport(PlayReportKind playReportKind, ulong pid, ReadOnlySpan<byte> gameRoomBuffer, ReadOnlySpan<byte> reportBuffer, Uid userId, bool withUserId = false)
{
if (withUserId)
{
if (userId.IsNull)
{
return PrepoResult.InvalidArgument;
}
}
if (gameRoomBuffer.Length > 31)
{
return PrepoResult.InvalidArgument;
}
string gameRoom = Encoding.UTF8.GetString(gameRoomBuffer).TrimEnd();
if (gameRoom == string.Empty)
{
return PrepoResult.InvalidState;
}
if (reportBuffer.Length == 0)
{
return PrepoResult.InvalidBufferSize;
}
// NOTE: The service calls arp:r using the pid to get the application id, if it fails PrepoResult.InvalidPid is returned.
// Reports are stored internally and an event is signaled to transmit them.
StringBuilder builder = new();
MessagePackObject deserializedReport = MessagePackSerializer.UnpackMessagePackObject(reportBuffer.ToArray());
builder.AppendLine();
builder.AppendLine("PlayReport log:");
builder.AppendLine($" Kind: {playReportKind}");
builder.AppendLine($" Pid: {pid}");
if (!userId.IsNull)
{
builder.AppendLine($" UserId: {userId}");
}
builder.AppendLine($" Room: {gameRoom}");
builder.AppendLine($" Report: {MessagePackObjectFormatter.Format(deserializedReport)}");
Logger.Info?.Print(LogClass.ServicePrepo, builder.ToString());
return Result.Success;
}
}
}

View File

@ -0,0 +1,49 @@
using Ryujinx.Horizon.Prepo.Types;
using Ryujinx.Horizon.Sdk.Sf.Hipc;
using Ryujinx.Horizon.Sdk.Sm;
namespace Ryujinx.Horizon.Prepo
{
class PrepoIpcServer
{
private const int PrepoMaxSessionsCount = 12;
private const int PrepoTotalMaxSessionsCount = PrepoMaxSessionsCount * 6;
private const int PointerBufferSize = 0x3800;
private const int MaxDomains = 64;
private const int MaxDomainObjects = 16;
private const int MaxPortsCount = 6;
private static readonly ManagerOptions _logManagerOptions = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false);
private SmApi _sm;
private PrepoServerManager _serverManager;
public void Initialize()
{
HeapAllocator allocator = new();
_sm = new SmApi();
_sm.Initialize().AbortOnFailure();
_serverManager = new PrepoServerManager(allocator, _sm, MaxPortsCount, _logManagerOptions, PrepoTotalMaxSessionsCount);
_serverManager.RegisterServer((int)PrepoPortIndex.Admin, ServiceName.Encode("prepo:a"), PrepoMaxSessionsCount); // 1.0.0-5.1.0
_serverManager.RegisterServer((int)PrepoPortIndex.Admin2, ServiceName.Encode("prepo:a2"), PrepoMaxSessionsCount); // 6.0.0+
_serverManager.RegisterServer((int)PrepoPortIndex.Manager, ServiceName.Encode("prepo:m"), PrepoMaxSessionsCount);
_serverManager.RegisterServer((int)PrepoPortIndex.User, ServiceName.Encode("prepo:u"), PrepoMaxSessionsCount);
_serverManager.RegisterServer((int)PrepoPortIndex.System, ServiceName.Encode("prepo:s"), PrepoMaxSessionsCount);
_serverManager.RegisterServer((int)PrepoPortIndex.Debug, ServiceName.Encode("prepo:d"), PrepoMaxSessionsCount); // 1.0.0
}
public void ServiceRequests()
{
_serverManager.ServiceRequests();
}
public void Shutdown()
{
_serverManager.Dispose();
}
}
}

View File

@ -0,0 +1,17 @@
namespace Ryujinx.Horizon.Prepo
{
class PrepoMain : IService
{
public static void Main(ServiceTable serviceTable)
{
PrepoIpcServer ipcServer = new();
ipcServer.Initialize();
serviceTable.SignalServiceReady();
ipcServer.ServiceRequests();
ipcServer.Shutdown();
}
}
}

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