Compare commits

...

36 Commits

Author SHA1 Message Date
merry
36d53819a4 NativeSignalHandler: Fix write flag (#4306)
* NativeSignalHandler: Fix write flag

* address comments
2023-01-19 00:13:17 +00:00
Andrey Sukharev
ae4324032a Optimize string memory usage. Use Spans and StringBuilders where possible (#3933)
* Optimize string memory usage. Use ReadOnlySpan<char> and StringBuilder where possible.

* Fix copypaste error

* Code generator review fixes

* Use if statement instead of switch

* Code style fixes

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

* Another code style fix

* Styling fix

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

* Styling fix

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

Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
Co-authored-by: Mary-nyan <thog@protonmail.com>
Co-authored-by: gdkchan <gab.dark.100@gmail.com>
2023-01-18 22:25:16 +00:00
Ac_K
f449895e6d HOS: Load RomFs by pid (#4301)
We currently loading only one RomFs at a time, which could be wrong if one day we want to load more than one guest at time.
This PR fixes that by loading romfs by pid.
2023-01-18 13:50:42 +00:00
TSRBerry
410be95ab6 Fix NRE when disposing AddressSpace with 4KB pages support (#4307) 2023-01-17 14:50:39 +00:00
merry
cff9046fc7 ConfigurationState: Default to Vulkan on macOS (#4299) 2023-01-17 05:32:08 +01:00
gdkchan
86fd0643c2 Implement support for page sizes > 4KB (#4252)
* Implement support for page sizes > 4KB

* Check and work around more alignment issues

* Was not meant to change this

* Use MemoryBlock.GetPageSize() value for signal handler code

* Do not take the path for private allocations if host supports 4KB pages

* Add Flags attribute on MemoryMapFlags

* Fix dirty region size with 16kb pages

Would accidentally report a size that was too high (generally 16k instead of 4k, uploading 4x as much data)

Co-authored-by: riperiperi <rhy3756547@hotmail.com>
2023-01-17 05:13:24 +01:00
Ac_K
43a83a401e Ava UI: Readd some infos to the GameList (#4302) 2023-01-17 04:57:21 +01:00
riperiperi
f0e27a23a5 Add short duration texture cache (#3754)
* Add short duration texture cache

This texture cache takes textures that lose their last pool reference and keeps them alive until the next frame, or until an incompatible overlap removes it. This is done since under certain circumstances, a texture's reference can be wiped from a pool despite it still being in use - though typically the reference will return when rendering the next frame.

While this may slightly increase texture memory usage when quickly going through a bunch of temporary textures, it's still bounded due to the overlap removal rule.

This greatly increases performance in Hyrule Warriors: Age of Calamity. It may positively affect some UE4 games which dip framerate severely under certain circumstances.

* Small optimization

* Don't forget this.

* Add short cache dictionary

* Address feedback

* Address some feedback
2023-01-17 04:39:46 +01:00
TSRBerry
e68650237d Ava: Fix Linux Vulkan renderer regression (#4303)
* ava: Fix Linux Vulkan renderer staying transparent

* ava: Minor Renderer cleanup

* Don't supress potential NRE warning

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

Co-authored-by: Ac_K <Acoustik666@gmail.com>
2023-01-16 03:59:41 +01:00
Ac_K
1faff14e73 UI: Fixes GTK sorting regression of #4294 2023-01-16 03:09:52 +01:00
Ac_K
784cf9d594 Ava UI: Renderer refactoring (#4297)
* Ava UI: `Renderer` refactoring

* Fix Vulkan CreateSurface
2023-01-16 01:14:01 +01:00
Ac_K
64263c5218 UI: Fix applications times (#4294)
* Fix applications times

* Add spaces

* Fix TimeString formatting
2023-01-16 00:11:16 +01:00
gdkchan
065c4e520d Specify image view usage flags on Vulkan (#4283)
* Specify image view usage flags on Vulkan

* PR feedback
2023-01-15 23:12:52 +01:00
Isaac Marovitz
139a930407 Implement missing service calls in pm (#4210)
* Implement `GetTitleId`

Fixes #2516

* Null check + Proper result code

* Better comment

* Implement `GetApplicationProcessId`

* Add TODOs

* Update Ryujinx.HLE/HOS/Services/Pm/IInformationInterface.cs

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

* Update Ryujinx.HLE/HOS/Services/Pm/IDebugMonitorInterface.cs

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

* Remove new function from KernelStatic

Co-authored-by: Ac_K <Acoustik666@gmail.com>
2023-01-15 22:16:24 +01:00
Isaac Marovitz
719dc97bbd Ava UI: TitleUpdateWindow Refactor (#4276)
* Start Refactor

* Dialogue opens

* Changes

* Switch to ListBox

* Fix bugs and stuff

* Fix spacing

* Implement OpenLocation

* Change icon

* Color

* Color

* Remove background

* Make no update the same height

* Fix height and smooth scroll

* Height

* Fix update selection

* Make window smaller

* Add back remove all button

* Make selection more obvious

* Hide selection bar on SaveManager

* Fix autoscroll

* Fix no update not staying selected

* Better file opener

* Fix

* Revert that

* Update Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.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>

* Log warning

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

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

Co-authored-by: Ac_K <Acoustik666@gmail.com>
2023-01-15 11:11:52 +00:00
merry
41bba5310a Audren: Implement polyphase upsampler (#4256)
* Audren: Implement polyphase upsampler

* prefer shifting to modulo

* prefer MathF

* fix nits

* rm ResampleForUpsampler

* oop

* Array20

* nits
2023-01-15 05:20:49 +01:00
Ac_K
8071c8c8c0 Ava UI: Fixes "Hide Cursor on Idle" for Windows (#4266)
* Ava: Fixes "Hide Cursor on Idle" for Windows

* Add check in MouseDriver and reduce the time of idling

* Fix linux error

* Change idle time everywhere for consistencies
2023-01-15 01:05:44 +01:00
gnisman
b402b4e7f6 Change GetPageSize to use Environment.SystemPageSize (#4291)
* Change GetPageSize to use Environment.SystemPageSize

* Fix PR comment
2023-01-14 15:37:04 -03:00
gdkchan
93df366b2c Fix texture flush from CPU WaitSync regression on OpenGL (#4289) 2023-01-14 11:23:57 -03:00
gdkchan
cd3a15aea5 Fix NRE when MemoryUnmappedHandler is called for a destroyed channel (#4285) 2023-01-14 00:16:06 -03:00
gdkchan
070136b3f7 Fix texture modified on CPU from GPU thread after being modified on GPU not being updated (#4284) 2023-01-13 23:46:45 -03:00
Ac_K
08ab47c6c0 Update Program.cs 2023-01-13 07:56:41 +01:00
Ac_K
85faa9d8fa Revert "Relax Vulkan requirements (#4228)" (#4279)
This reverts commit dca5b14493.
2023-01-13 06:04:59 +00:00
gdkchan
dca5b14493 Relax Vulkan requirements (#4228) 2023-01-13 06:09:48 +01:00
Ac_K
4d2c8e2a44 Prepo: Fix SaveSystemReport* IPC definitions (#4278)
* Prepo: Fix SaveSystemReport IPC definitions

* Follow original code

* Fix args index in HipcGenerator

* Addresses feedback

* oops
2023-01-13 01:50:14 +01:00
riperiperi
8fa248ceb4 Vulkan: Add workarounds for MoltenVK (#4202)
* Add MVK basics.

* Use appropriate output attribute types

* 4kb vertex alignment, bunch of fixes

* Add reduced shader precision mode for mvk.

* Disable ASTC on MVK for now

* Only request robustnes2 when it is available.

* It's just the one feature actually

* Add triangle fan conversion

* Allow NullDescriptor on MVK for some reason.

* Force safe blit on MoltenVK

* Use ASTC only when formats are all available.

* Disable multilevel 3d texture views

* Filter duplicate render targets (on backend)

* Add Automatic MoltenVK Configuration

* Do not create color attachment views with formats that are not RT compatible

* Make sure that the host format matches the vertex shader input types for invalid/unknown guest formats

* FIx rebase for Vertex Attrib State

* Fix 4b alignment for vertex

* Use asynchronous queue submits for MVK

* Ensure color clear shader has correct output type

* Update MoltenVK config

* Always use MoltenVK workarounds on MacOS

* Make MVK supersede all vendors

* Fix rebase

* Various fixes on rebase

* Get portability flags from extension

* Fix some minor rebasing issues

* Style change

* Use LibraryImport for MVKConfiguration

* Rename MoltenVK vendor to Apple

Intel and AMD GPUs on moltenvk report with the those vendors - only apple silicon reports with vendor 0x106B.

* Fix features2 rebase conflict

* Rename fragment output type

* Add missing check for fragment output types

Might have caused the crash in MK8

* Only do fragment output specialization on MoltenVK

* Avoid copy when passing capabilities

* Self feedback

* Address feedback

Co-authored-by: gdk <gab.dark.100@gmail.com>
Co-authored-by: nastys <nastys@users.noreply.github.com>
2023-01-13 01:31:21 +01:00
Ac_K
30862b5ffd ava: Reorder settings of Resolution Scaler (#4270) 2023-01-13 00:07:53 +01:00
Isaac Marovitz
9f57747c57 Ava UI: Various Fixes (#4268)
* Fix saves disappearing

* Better size formatter

* Move TextBox alignment fix to Styles

* Fix bug

* Left align

* Add border

* Update Ryujinx.Ava/UI/Models/SaveModel.cs

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

* Update Ryujinx.Ava/UI/Models/SaveModel.cs

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

* Update Ryujinx.Ava/UI/Models/SaveModel.cs

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

* Whitespace

Co-authored-by: Ac_K <Acoustik666@gmail.com>
2023-01-12 12:23:24 +00:00
Isaac Marovitz
fe29a2ff6e Ava UI: Settings Adjustments (#4273)
* Visual adjustments

* Match border to rest of app

* Fix overlapping controls

* Fix

* Fix
2023-01-12 12:09:32 +00:00
merry
e9a173e00c Ptc: Check process architecture (#4272) 2023-01-12 07:50:45 +00:00
merry
a11784fcbf Arm64: Cpu feature detection (#4264)
* Arm64: Cpu feature detection

* Ptc: Add Arm64 feature info

* nits

* simplify CheckSysctlName

* restore some macos flags

* feedback
2023-01-12 08:05:18 +01:00
Ac_K
fd36c8deca lm: Handle Tail flag in LogPacket (#4274)
* lm: Handle TailFlag in LogPacket

* Addresses feedback
2023-01-12 07:42:05 +01:00
Ac_K
70638340b3 Ava UI: Move Ava logging to Logger.Debug (#4255)
* Ava: Move Ava logging to Logger.Debug

Since #4231 we currently redirect Avalonia logs to our Logger, which is pretty nice. But since it uses our Logging level too, it now leads to a massive flood in our Log files.
To avoid that, I've included all `AvaLogLevel` to the log message, and make all Ava Logs using `Logger.Debug`.

* Logs errors to Error and other to Debug

* missing level

* keep var
2023-01-11 23:27:26 +01:00
Ac_K
4b495f3333 Ava UI: Fixes PerformanceCheck condition (#4271) 2023-01-11 20:47:15 +00:00
Isaac Marovitz
934b5a64e5 Ava GUI: User Profile Manager + Other Fixes (#4166)
* Fix redundancies

* Add back elses

* Loading Screen fixes

* Redesign User Profile Manager

- Backported long selection bar in Grid/List view not working
- Backported UserSelector is jank

* Fix SelectionIndicator

* Fix DataType

* Fix SaveManager bug

* Remove debug log

* Load saves on UIThread

* Reduce UI thread blocking

* Fix locale keys

* Use block namespaces

* Fix close button width

* Make UserProfile ordering consistent

* Alphabetical order

* Adjust layout, remove green circle for blue selector

* Fix some inconsistencies

* Fix no inital selected profile

* Adjust appearance of edit button

* Adjust SaveManager

* Remove redundant warning dialog

* Make firmware avatar selector clearer

* View redesign again :hero_depressed:

* Consistency adjustments

* Adjust margins

* Make `UserProfileImageSelector` consistent

* Make `UserFirmwareAvatarSelector` consistent

* Fix long grid view selector

* Switch case

* Remove long selection bar

Handled in #4178

* Consistency

* Started dialog titles

* Fixes

* Remaining titles

* Update Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml

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

* Fix build

* Hide UserRecoverer if no LostProfiles are found

* UserEditor Avatar Placeholder

* Watermark + locale adjustment

* Border radius

* Remove unnecessary styles

* Fix firmware avatar image order

* Cleanup `ColorPickerButton`

* Make `UserId` copy/paste able

* Make `FirmwareAvatarSelector` 6 images wide

* Make selection bar better

* Unsaved changes dialogue

* Fix indentation

* Remove extra check

* Address suggestions

* Reorganise

- Remove unused views
- Rename views to match convention
- Fix weird namespacing

* Update Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml

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

* Update Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml

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

* UserRecovererView empty placeholder

* Update Ryujinx.Ava/UI/Views/User/UserSelectorView.axaml.cs

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

* Update Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml.cs

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

* Update Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml.cs

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

* Update Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml.cs

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

* Update Ryujinx.Ava/UI/Views/User/UserRecovererView.axaml.cs

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

* Update Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml.cs

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

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

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

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

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

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

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

* Update Ryujinx.Ava/UI/Models/UserProfile.cs

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

* Update Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml.cs

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

* Update Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml.cs

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

* Remove AddModel

* Update Ryujinx.Ava/Assets/Locales/en_US.json

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

* Fix bug

Co-authored-by: Mary-nyan <thog@protonmail.com>
Co-authored-by: Ac_K <Acoustik666@gmail.com>
2023-01-11 06:20:19 +01:00
Ac_K
cee667b491 Ava: Fixes Update count in heading (#4265) 2023-01-11 05:39:25 +01:00
198 changed files with 6716 additions and 3181 deletions

View File

@@ -0,0 +1,185 @@
using System;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics.Arm;
using System.Runtime.Versioning;
namespace ARMeilleure.CodeGen.Arm64
{
static partial class HardwareCapabilities
{
static HardwareCapabilities()
{
if (!ArmBase.Arm64.IsSupported)
{
return;
}
if (OperatingSystem.IsLinux())
{
LinuxFeatureInfoHwCap = (LinuxFeatureFlagsHwCap)getauxval(AT_HWCAP);
LinuxFeatureInfoHwCap2 = (LinuxFeatureFlagsHwCap2)getauxval(AT_HWCAP2);
}
if (OperatingSystem.IsMacOS())
{
for (int i = 0; i < _sysctlNames.Length; i++)
{
if (CheckSysctlName(_sysctlNames[i]))
{
MacOsFeatureInfo |= (MacOsFeatureFlags)(1 << i);
}
}
}
}
#region Linux
private const ulong AT_HWCAP = 16;
private const ulong AT_HWCAP2 = 26;
[LibraryImport("libc", SetLastError = true)]
private static partial ulong getauxval(ulong type);
[Flags]
public enum LinuxFeatureFlagsHwCap : ulong
{
Fp = 1 << 0,
Asimd = 1 << 1,
Evtstrm = 1 << 2,
Aes = 1 << 3,
Pmull = 1 << 4,
Sha1 = 1 << 5,
Sha2 = 1 << 6,
Crc32 = 1 << 7,
Atomics = 1 << 8,
FpHp = 1 << 9,
AsimdHp = 1 << 10,
CpuId = 1 << 11,
AsimdRdm = 1 << 12,
Jscvt = 1 << 13,
Fcma = 1 << 14,
Lrcpc = 1 << 15,
DcpOp = 1 << 16,
Sha3 = 1 << 17,
Sm3 = 1 << 18,
Sm4 = 1 << 19,
AsimdDp = 1 << 20,
Sha512 = 1 << 21,
Sve = 1 << 22,
AsimdFhm = 1 << 23,
Dit = 1 << 24,
Uscat = 1 << 25,
Ilrcpc = 1 << 26,
FlagM = 1 << 27,
Ssbs = 1 << 28,
Sb = 1 << 29,
Paca = 1 << 30,
Pacg = 1UL << 31
}
[Flags]
public enum LinuxFeatureFlagsHwCap2 : ulong
{
Dcpodp = 1 << 0,
Sve2 = 1 << 1,
SveAes = 1 << 2,
SvePmull = 1 << 3,
SveBitperm = 1 << 4,
SveSha3 = 1 << 5,
SveSm4 = 1 << 6,
FlagM2 = 1 << 7,
Frint = 1 << 8,
SveI8mm = 1 << 9,
SveF32mm = 1 << 10,
SveF64mm = 1 << 11,
SveBf16 = 1 << 12,
I8mm = 1 << 13,
Bf16 = 1 << 14,
Dgh = 1 << 15,
Rng = 1 << 16,
Bti = 1 << 17,
Mte = 1 << 18,
Ecv = 1 << 19,
Afp = 1 << 20,
Rpres = 1 << 21,
Mte3 = 1 << 22,
Sme = 1 << 23,
Sme_i16i64 = 1 << 24,
Sme_f64f64 = 1 << 25,
Sme_i8i32 = 1 << 26,
Sme_f16f32 = 1 << 27,
Sme_b16f32 = 1 << 28,
Sme_f32f32 = 1 << 29,
Sme_fa64 = 1 << 30,
Wfxt = 1UL << 31,
Ebf16 = 1UL << 32,
Sve_Ebf16 = 1UL << 33,
Cssc = 1UL << 34,
Rprfm = 1UL << 35,
Sve2p1 = 1UL << 36
}
public static LinuxFeatureFlagsHwCap LinuxFeatureInfoHwCap { get; } = 0;
public static LinuxFeatureFlagsHwCap2 LinuxFeatureInfoHwCap2 { get; } = 0;
#endregion
#region macOS
[LibraryImport("libSystem.dylib", SetLastError = true)]
private static unsafe partial int sysctlbyname([MarshalAs(UnmanagedType.LPStr)] string name, out int oldValue, ref ulong oldSize, IntPtr newValue, ulong newValueSize);
[SupportedOSPlatform("macos")]
private static bool CheckSysctlName(string name)
{
ulong size = sizeof(int);
if (sysctlbyname(name, out int val, ref size, IntPtr.Zero, 0) == 0 && size == sizeof(int))
{
return val != 0;
}
return false;
}
private static string[] _sysctlNames = new string[]
{
"hw.optional.floatingpoint",
"hw.optional.AdvSIMD",
"hw.optional.arm.FEAT_FP16",
"hw.optional.arm.FEAT_AES",
"hw.optional.arm.FEAT_PMULL",
"hw.optional.arm.FEAT_LSE",
"hw.optional.armv8_crc32",
"hw.optional.arm.FEAT_SHA1",
"hw.optional.arm.FEAT_SHA256"
};
[Flags]
public enum MacOsFeatureFlags
{
Fp = 1 << 0,
AdvSimd = 1 << 1,
Fp16 = 1 << 2,
Aes = 1 << 3,
Pmull = 1 << 4,
Lse = 1 << 5,
Crc32 = 1 << 6,
Sha1 = 1 << 7,
Sha256 = 1 << 8
}
public static MacOsFeatureFlags MacOsFeatureInfo { get; } = 0;
#endregion
public static bool SupportsAdvSimd => LinuxFeatureInfoHwCap.HasFlag(LinuxFeatureFlagsHwCap.Asimd) || MacOsFeatureInfo.HasFlag(MacOsFeatureFlags.AdvSimd);
public static bool SupportsAes => LinuxFeatureInfoHwCap.HasFlag(LinuxFeatureFlagsHwCap.Aes) || MacOsFeatureInfo.HasFlag(MacOsFeatureFlags.Aes);
public static bool SupportsPmull => LinuxFeatureInfoHwCap.HasFlag(LinuxFeatureFlagsHwCap.Pmull) || MacOsFeatureInfo.HasFlag(MacOsFeatureFlags.Pmull);
public static bool SupportsLse => LinuxFeatureInfoHwCap.HasFlag(LinuxFeatureFlagsHwCap.Atomics) || MacOsFeatureInfo.HasFlag(MacOsFeatureFlags.Lse);
public static bool SupportsCrc32 => LinuxFeatureInfoHwCap.HasFlag(LinuxFeatureFlagsHwCap.Crc32) || MacOsFeatureInfo.HasFlag(MacOsFeatureFlags.Crc32);
public static bool SupportsSha1 => LinuxFeatureInfoHwCap.HasFlag(LinuxFeatureFlagsHwCap.Sha1) || MacOsFeatureInfo.HasFlag(MacOsFeatureFlags.Sha1);
public static bool SupportsSha256 => LinuxFeatureInfoHwCap.HasFlag(LinuxFeatureFlagsHwCap.Sha2) || MacOsFeatureInfo.HasFlag(MacOsFeatureFlags.Sha256);
}
}

View File

@@ -1339,7 +1339,7 @@ namespace ARMeilleure.Decoders
private static void SetT32(string encoding, InstName name, InstEmitter emitter, MakeOp makeOp) private static void SetT32(string encoding, InstName name, InstEmitter emitter, MakeOp makeOp)
{ {
string reversedEncoding = encoding.Substring(16) + encoding.Substring(0, 16); string reversedEncoding = $"{encoding.AsSpan(16)}{encoding.AsSpan(0, 16)}";
MakeOp reversedMakeOp = MakeOp reversedMakeOp =
(inst, address, opCode) (inst, address, opCode)
=> makeOp(inst, address, (int)BitOperations.RotateRight((uint)opCode, 16)); => makeOp(inst, address, (int)BitOperations.RotateRight((uint)opCode, 16));
@@ -1353,7 +1353,7 @@ namespace ARMeilleure.Decoders
string thumbEncoding = encoding; string thumbEncoding = encoding;
if (thumbEncoding.StartsWith("<<<<")) if (thumbEncoding.StartsWith("<<<<"))
{ {
thumbEncoding = "1110" + thumbEncoding.Substring(4); thumbEncoding = $"1110{thumbEncoding.AsSpan(4)}";
} }
SetT32(thumbEncoding, name, emitter, makeOpT32); SetT32(thumbEncoding, name, emitter, makeOpT32);
} }
@@ -1365,19 +1365,19 @@ namespace ARMeilleure.Decoders
string thumbEncoding = encoding; string thumbEncoding = encoding;
if (thumbEncoding.StartsWith("11110100")) if (thumbEncoding.StartsWith("11110100"))
{ {
thumbEncoding = "11111001" + encoding.Substring(8); thumbEncoding = $"11111001{encoding.AsSpan(8)}";
} }
else if (thumbEncoding.StartsWith("1111001x")) else if (thumbEncoding.StartsWith("1111001x"))
{ {
thumbEncoding = "111x1111" + encoding.Substring(8); thumbEncoding = $"111x1111{encoding.AsSpan(8)}";
} }
else if (thumbEncoding.StartsWith("11110010")) else if (thumbEncoding.StartsWith("11110010"))
{ {
thumbEncoding = "11101111" + encoding.Substring(8); thumbEncoding = $"11101111{encoding.AsSpan(8)}";
} }
else if (thumbEncoding.StartsWith("11110011")) else if (thumbEncoding.StartsWith("11110011"))
{ {
thumbEncoding = "11111111" + encoding.Substring(8); thumbEncoding = $"11111111{encoding.AsSpan(8)}";
} }
else else
{ {

View File

@@ -2556,7 +2556,7 @@ namespace ARMeilleure.Instructions
{ {
OpCodeSimdReg op = (OpCodeSimdReg)context.CurrOp; OpCodeSimdReg op = (OpCodeSimdReg)context.CurrOp;
if (Optimizations.UseAdvSimd && false) // Not supported by all Arm CPUs. if (Optimizations.UseArm64Pmull)
{ {
InstEmitSimdHelperArm64.EmitVectorBinaryOp(context, Intrinsic.Arm64PmullV); InstEmitSimdHelperArm64.EmitVectorBinaryOp(context, Intrinsic.Arm64PmullV);
} }

View File

@@ -4,5 +4,7 @@
{ {
IJitMemoryBlock Allocate(ulong size); IJitMemoryBlock Allocate(ulong size);
IJitMemoryBlock Reserve(ulong size); IJitMemoryBlock Reserve(ulong size);
ulong GetPageSize();
} }
} }

View File

@@ -1,8 +1,10 @@
using ARMeilleure.CodeGen.X86;
using System.Runtime.Intrinsics.Arm; using System.Runtime.Intrinsics.Arm;
namespace ARMeilleure namespace ARMeilleure
{ {
using Arm64HardwareCapabilities = ARMeilleure.CodeGen.Arm64.HardwareCapabilities;
using X86HardwareCapabilities = ARMeilleure.CodeGen.X86.HardwareCapabilities;
public static class Optimizations public static class Optimizations
{ {
public static bool FastFP { get; set; } = true; public static bool FastFP { get; set; } = true;
@@ -11,6 +13,7 @@ namespace ARMeilleure
public static bool UseUnmanagedDispatchLoop { get; set; } = true; public static bool UseUnmanagedDispatchLoop { get; set; } = true;
public static bool UseAdvSimdIfAvailable { get; set; } = true; public static bool UseAdvSimdIfAvailable { get; set; } = true;
public static bool UseArm64PmullIfAvailable { get; set; } = true;
public static bool UseSseIfAvailable { get; set; } = true; public static bool UseSseIfAvailable { get; set; } = true;
public static bool UseSse2IfAvailable { get; set; } = true; public static bool UseSse2IfAvailable { get; set; } = true;
@@ -29,25 +32,26 @@ namespace ARMeilleure
public static bool ForceLegacySse public static bool ForceLegacySse
{ {
get => HardwareCapabilities.ForceLegacySse; get => X86HardwareCapabilities.ForceLegacySse;
set => HardwareCapabilities.ForceLegacySse = value; set => X86HardwareCapabilities.ForceLegacySse = value;
} }
internal static bool UseAdvSimd => UseAdvSimdIfAvailable && AdvSimd.IsSupported; internal static bool UseAdvSimd => UseAdvSimdIfAvailable && Arm64HardwareCapabilities.SupportsAdvSimd;
internal static bool UseArm64Pmull => UseArm64PmullIfAvailable && Arm64HardwareCapabilities.SupportsPmull;
internal static bool UseSse => UseSseIfAvailable && HardwareCapabilities.SupportsSse; internal static bool UseSse => UseSseIfAvailable && X86HardwareCapabilities.SupportsSse;
internal static bool UseSse2 => UseSse2IfAvailable && HardwareCapabilities.SupportsSse2; internal static bool UseSse2 => UseSse2IfAvailable && X86HardwareCapabilities.SupportsSse2;
internal static bool UseSse3 => UseSse3IfAvailable && HardwareCapabilities.SupportsSse3; internal static bool UseSse3 => UseSse3IfAvailable && X86HardwareCapabilities.SupportsSse3;
internal static bool UseSsse3 => UseSsse3IfAvailable && HardwareCapabilities.SupportsSsse3; internal static bool UseSsse3 => UseSsse3IfAvailable && X86HardwareCapabilities.SupportsSsse3;
internal static bool UseSse41 => UseSse41IfAvailable && HardwareCapabilities.SupportsSse41; internal static bool UseSse41 => UseSse41IfAvailable && X86HardwareCapabilities.SupportsSse41;
internal static bool UseSse42 => UseSse42IfAvailable && HardwareCapabilities.SupportsSse42; internal static bool UseSse42 => UseSse42IfAvailable && X86HardwareCapabilities.SupportsSse42;
internal static bool UsePopCnt => UsePopCntIfAvailable && HardwareCapabilities.SupportsPopcnt; internal static bool UsePopCnt => UsePopCntIfAvailable && X86HardwareCapabilities.SupportsPopcnt;
internal static bool UseAvx => UseAvxIfAvailable && HardwareCapabilities.SupportsAvx && !ForceLegacySse; internal static bool UseAvx => UseAvxIfAvailable && X86HardwareCapabilities.SupportsAvx && !ForceLegacySse;
internal static bool UseF16c => UseF16cIfAvailable && HardwareCapabilities.SupportsF16c; internal static bool UseF16c => UseF16cIfAvailable && X86HardwareCapabilities.SupportsF16c;
internal static bool UseFma => UseFmaIfAvailable && HardwareCapabilities.SupportsFma; internal static bool UseFma => UseFmaIfAvailable && X86HardwareCapabilities.SupportsFma;
internal static bool UseAesni => UseAesniIfAvailable && HardwareCapabilities.SupportsAesni; internal static bool UseAesni => UseAesniIfAvailable && X86HardwareCapabilities.SupportsAesni;
internal static bool UsePclmulqdq => UsePclmulqdqIfAvailable && HardwareCapabilities.SupportsPclmulqdq; internal static bool UsePclmulqdq => UsePclmulqdqIfAvailable && X86HardwareCapabilities.SupportsPclmulqdq;
internal static bool UseSha => UseShaIfAvailable && HardwareCapabilities.SupportsSha; internal static bool UseSha => UseShaIfAvailable && X86HardwareCapabilities.SupportsSha;
internal static bool UseGfni => UseGfniIfAvailable && HardwareCapabilities.SupportsGfni; internal static bool UseGfni => UseGfniIfAvailable && X86HardwareCapabilities.SupportsGfni;
} }
} }

View File

@@ -71,8 +71,8 @@ namespace ARMeilleure.Signal
private const uint EXCEPTION_ACCESS_VIOLATION = 0xc0000005; private const uint EXCEPTION_ACCESS_VIOLATION = 0xc0000005;
private static ulong _pageSize = GetPageSize(); private static ulong _pageSize;
private static ulong _pageMask = _pageSize - 1; private static ulong _pageMask;
private static IntPtr _handlerConfig; private static IntPtr _handlerConfig;
private static IntPtr _signalHandlerPtr; private static IntPtr _signalHandlerPtr;
@@ -81,19 +81,6 @@ namespace ARMeilleure.Signal
private static readonly object _lock = new object(); private static readonly object _lock = new object();
private static bool _initialized; private static bool _initialized;
private static ulong GetPageSize()
{
// TODO: This needs to be based on the current memory manager configuration.
if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{
return 1UL << 14;
}
else
{
return 1UL << 12;
}
}
static NativeSignalHandler() static NativeSignalHandler()
{ {
_handlerConfig = Marshal.AllocHGlobal(Unsafe.SizeOf<SignalHandlerConfig>()); _handlerConfig = Marshal.AllocHGlobal(Unsafe.SizeOf<SignalHandlerConfig>());
@@ -102,12 +89,12 @@ namespace ARMeilleure.Signal
config = new SignalHandlerConfig(); config = new SignalHandlerConfig();
} }
public static void InitializeJitCache(IJitMemoryAllocator allocator) public static void Initialize(IJitMemoryAllocator allocator)
{ {
JitCache.Initialize(allocator); JitCache.Initialize(allocator);
} }
public static void InitializeSignalHandler(Func<IntPtr, IntPtr, IntPtr> customSignalHandlerFactory = null) public static void InitializeSignalHandler(ulong pageSize, Func<IntPtr, IntPtr, IntPtr> customSignalHandlerFactory = null)
{ {
if (_initialized) return; if (_initialized) return;
@@ -115,16 +102,13 @@ namespace ARMeilleure.Signal
{ {
if (_initialized) return; if (_initialized) return;
_pageSize = pageSize;
_pageMask = pageSize - 1;
ref SignalHandlerConfig config = ref GetConfigRef(); ref SignalHandlerConfig config = ref GetConfigRef();
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
{ {
// Unix siginfo struct locations.
// NOTE: These are incredibly likely to be different between kernel version and architectures.
config.StructAddressOffset = OperatingSystem.IsMacOS() ? 24 : 16; // si_addr
config.StructWriteOffset = 8; // si_code
_signalHandlerPtr = Marshal.GetFunctionPointerForDelegate(GenerateUnixSignalHandler(_handlerConfig)); _signalHandlerPtr = Marshal.GetFunctionPointerForDelegate(GenerateUnixSignalHandler(_handlerConfig));
if (customSignalHandlerFactory != null) if (customSignalHandlerFactory != null)
@@ -261,18 +245,88 @@ namespace ARMeilleure.Signal
return context.Copy(inRegionLocal); return context.Copy(inRegionLocal);
} }
private static Operand GenerateUnixFaultAddress(EmitterContext context, Operand sigInfoPtr)
{
ulong structAddressOffset = OperatingSystem.IsMacOS() ? 24ul : 16ul; // si_addr
return context.Load(OperandType.I64, context.Add(sigInfoPtr, Const(structAddressOffset)));
}
private static Operand GenerateUnixWriteFlag(EmitterContext context, Operand ucontextPtr)
{
if (OperatingSystem.IsMacOS())
{
const ulong mcontextOffset = 48; // uc_mcontext
Operand ctxPtr = context.Load(OperandType.I64, context.Add(ucontextPtr, Const(mcontextOffset)));
if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{
const ulong esrOffset = 8; // __es.__esr
Operand esr = context.Load(OperandType.I64, context.Add(ctxPtr, Const(esrOffset)));
return context.BitwiseAnd(esr, Const(0x40ul));
}
if (RuntimeInformation.ProcessArchitecture == Architecture.X64)
{
const ulong errOffset = 4; // __es.__err
Operand err = context.Load(OperandType.I64, context.Add(ctxPtr, Const(errOffset)));
return context.BitwiseAnd(err, Const(2ul));
}
}
else if (OperatingSystem.IsLinux())
{
if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{
Operand auxPtr = context.AllocateLocal(OperandType.I64);
Operand loopLabel = Label();
Operand successLabel = Label();
const ulong auxOffset = 464; // uc_mcontext.__reserved
const uint esrMagic = 0x45535201;
context.Copy(auxPtr, context.Add(ucontextPtr, Const(auxOffset)));
context.MarkLabel(loopLabel);
// _aarch64_ctx::magic
Operand magic = context.Load(OperandType.I32, auxPtr);
// _aarch64_ctx::size
Operand size = context.Load(OperandType.I32, context.Add(auxPtr, Const(4ul)));
context.BranchIf(successLabel, magic, Const(esrMagic), Comparison.Equal);
context.Copy(auxPtr, context.Add(auxPtr, context.ZeroExtend32(OperandType.I64, size)));
context.Branch(loopLabel);
context.MarkLabel(successLabel);
// esr_context::esr
Operand esr = context.Load(OperandType.I64, context.Add(auxPtr, Const(8ul)));
return context.BitwiseAnd(esr, Const(0x40ul));
}
if (RuntimeInformation.ProcessArchitecture == Architecture.X64)
{
const int errOffset = 192; // uc_mcontext.gregs[REG_ERR]
Operand err = context.Load(OperandType.I64, context.Add(ucontextPtr, Const(errOffset)));
return context.BitwiseAnd(err, Const(2ul));
}
}
throw new PlatformNotSupportedException();
}
private static UnixExceptionHandler GenerateUnixSignalHandler(IntPtr signalStructPtr) private static UnixExceptionHandler GenerateUnixSignalHandler(IntPtr signalStructPtr)
{ {
EmitterContext context = new EmitterContext(); EmitterContext context = new EmitterContext();
// (int sig, SigInfo* sigInfo, void* ucontext) // (int sig, SigInfo* sigInfo, void* ucontext)
Operand sigInfoPtr = context.LoadArgument(OperandType.I64, 1); Operand sigInfoPtr = context.LoadArgument(OperandType.I64, 1);
Operand ucontextPtr = context.LoadArgument(OperandType.I64, 2);
Operand structAddressOffset = context.Load(OperandType.I64, Const((ulong)signalStructPtr + StructAddressOffset)); Operand faultAddress = GenerateUnixFaultAddress(context, sigInfoPtr);
Operand structWriteOffset = context.Load(OperandType.I64, Const((ulong)signalStructPtr + StructWriteOffset)); Operand writeFlag = GenerateUnixWriteFlag(context, ucontextPtr);
Operand faultAddress = context.Load(OperandType.I64, context.Add(sigInfoPtr, context.ZeroExtend32(OperandType.I64, structAddressOffset)));
Operand writeFlag = context.Load(OperandType.I64, context.Add(sigInfoPtr, context.ZeroExtend32(OperandType.I64, structWriteOffset)));
Operand isWrite = context.ICompareNotEqual(writeFlag, Const(0L)); // Normalize to 0/1. Operand isWrite = context.ICompareNotEqual(writeFlag, Const(0L)); // Normalize to 0/1.

View File

@@ -1,7 +1,6 @@
using ARMeilleure.CodeGen; using ARMeilleure.CodeGen;
using ARMeilleure.CodeGen.Linking; using ARMeilleure.CodeGen.Linking;
using ARMeilleure.CodeGen.Unwinding; using ARMeilleure.CodeGen.Unwinding;
using ARMeilleure.CodeGen.X86;
using ARMeilleure.Common; using ARMeilleure.Common;
using ARMeilleure.Memory; using ARMeilleure.Memory;
using Ryujinx.Common; using Ryujinx.Common;
@@ -22,12 +21,15 @@ using static ARMeilleure.Translation.PTC.PtcFormatter;
namespace ARMeilleure.Translation.PTC namespace ARMeilleure.Translation.PTC
{ {
using Arm64HardwareCapabilities = ARMeilleure.CodeGen.Arm64.HardwareCapabilities;
using X86HardwareCapabilities = ARMeilleure.CodeGen.X86.HardwareCapabilities;
class Ptc : IPtcLoadState class Ptc : IPtcLoadState
{ {
private const string OuterHeaderMagicString = "PTCohd\0\0"; private const string OuterHeaderMagicString = "PTCohd\0\0";
private const string InnerHeaderMagicString = "PTCihd\0\0"; private const string InnerHeaderMagicString = "PTCihd\0\0";
private const uint InternalVersion = 4114; //! To be incremented manually for each change to the ARMeilleure project. private const uint InternalVersion = 4272; //! To be incremented manually for each change to the ARMeilleure project.
private const string ActualDir = "0"; private const string ActualDir = "0";
private const string BackupDir = "1"; private const string BackupDir = "1";
@@ -181,8 +183,8 @@ namespace ARMeilleure.Translation.PTC
private void PreLoad() private void PreLoad()
{ {
string fileNameActual = string.Concat(CachePathActual, ".cache"); string fileNameActual = $"{CachePathActual}.cache";
string fileNameBackup = string.Concat(CachePathBackup, ".cache"); string fileNameBackup = $"{CachePathBackup}.cache";
FileInfo fileInfoActual = new FileInfo(fileNameActual); FileInfo fileInfoActual = new FileInfo(fileNameActual);
FileInfo fileInfoBackup = new FileInfo(fileNameBackup); FileInfo fileInfoBackup = new FileInfo(fileNameBackup);
@@ -259,6 +261,13 @@ namespace ARMeilleure.Translation.PTC
return false; return false;
} }
if (outerHeader.Architecture != (uint)RuntimeInformation.ProcessArchitecture)
{
InvalidateCompressedStream(compressedStream);
return false;
}
IntPtr intPtr = IntPtr.Zero; IntPtr intPtr = IntPtr.Zero;
try try
@@ -391,8 +400,8 @@ namespace ARMeilleure.Translation.PTC
try try
{ {
string fileNameActual = string.Concat(CachePathActual, ".cache"); string fileNameActual = $"{CachePathActual}.cache";
string fileNameBackup = string.Concat(CachePathBackup, ".cache"); string fileNameBackup = $"{CachePathBackup}.cache";
FileInfo fileInfoActual = new FileInfo(fileNameActual); FileInfo fileInfoActual = new FileInfo(fileNameActual);
@@ -435,6 +444,7 @@ namespace ARMeilleure.Translation.PTC
outerHeader.FeatureInfo = GetFeatureInfo(); outerHeader.FeatureInfo = GetFeatureInfo();
outerHeader.MemoryManagerMode = GetMemoryManagerMode(); outerHeader.MemoryManagerMode = GetMemoryManagerMode();
outerHeader.OSPlatform = GetOSPlatform(); outerHeader.OSPlatform = GetOSPlatform();
outerHeader.Architecture = (uint)RuntimeInformation.ProcessArchitecture;
outerHeader.UncompressedStreamSize = outerHeader.UncompressedStreamSize =
(long)Unsafe.SizeOf<InnerHeader>() + (long)Unsafe.SizeOf<InnerHeader>() +
@@ -951,12 +961,27 @@ namespace ARMeilleure.Translation.PTC
} }
private static FeatureInfo GetFeatureInfo() private static FeatureInfo GetFeatureInfo()
{
if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{ {
return new FeatureInfo( return new FeatureInfo(
(uint)HardwareCapabilities.FeatureInfo1Ecx, (ulong)Arm64HardwareCapabilities.LinuxFeatureInfoHwCap,
(uint)HardwareCapabilities.FeatureInfo1Edx, (ulong)Arm64HardwareCapabilities.LinuxFeatureInfoHwCap2,
(uint)HardwareCapabilities.FeatureInfo7Ebx, (ulong)Arm64HardwareCapabilities.MacOsFeatureInfo,
(uint)HardwareCapabilities.FeatureInfo7Ecx); 0);
}
else if (RuntimeInformation.ProcessArchitecture == Architecture.X64)
{
return new FeatureInfo(
(ulong)X86HardwareCapabilities.FeatureInfo1Ecx,
(ulong)X86HardwareCapabilities.FeatureInfo1Edx,
(ulong)X86HardwareCapabilities.FeatureInfo7Ebx,
(ulong)X86HardwareCapabilities.FeatureInfo7Ecx);
}
else
{
return new FeatureInfo(0, 0, 0, 0);
}
} }
private byte GetMemoryManagerMode() private byte GetMemoryManagerMode()
@@ -976,7 +1001,7 @@ namespace ARMeilleure.Translation.PTC
return osPlatform; return osPlatform;
} }
[StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 58*/)] [StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 78*/)]
private struct OuterHeader private struct OuterHeader
{ {
public ulong Magic; public ulong Magic;
@@ -987,6 +1012,7 @@ namespace ARMeilleure.Translation.PTC
public FeatureInfo FeatureInfo; public FeatureInfo FeatureInfo;
public byte MemoryManagerMode; public byte MemoryManagerMode;
public uint OSPlatform; public uint OSPlatform;
public uint Architecture;
public long UncompressedStreamSize; public long UncompressedStreamSize;
@@ -1007,8 +1033,8 @@ namespace ARMeilleure.Translation.PTC
} }
} }
[StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 16*/)] [StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 32*/)]
private record struct FeatureInfo(uint FeatureInfo0, uint FeatureInfo1, uint FeatureInfo2, uint FeatureInfo3); private record struct FeatureInfo(ulong FeatureInfo0, ulong FeatureInfo1, ulong FeatureInfo2, ulong FeatureInfo3);
[StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 128*/)] [StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 128*/)]
private struct InnerHeader private struct InnerHeader

View File

@@ -125,8 +125,8 @@ namespace ARMeilleure.Translation.PTC
{ {
_lastHash = default; _lastHash = default;
string fileNameActual = string.Concat(_ptc.CachePathActual, ".info"); string fileNameActual = $"{_ptc.CachePathActual}.info";
string fileNameBackup = string.Concat(_ptc.CachePathBackup, ".info"); string fileNameBackup = $"{_ptc.CachePathBackup}.info";
FileInfo fileInfoActual = new FileInfo(fileNameActual); FileInfo fileInfoActual = new FileInfo(fileNameActual);
FileInfo fileInfoBackup = new FileInfo(fileNameBackup); FileInfo fileInfoBackup = new FileInfo(fileNameBackup);
@@ -246,8 +246,8 @@ namespace ARMeilleure.Translation.PTC
{ {
_waitEvent.Reset(); _waitEvent.Reset();
string fileNameActual = string.Concat(_ptc.CachePathActual, ".info"); string fileNameActual = $"{_ptc.CachePathActual}.info";
string fileNameBackup = string.Concat(_ptc.CachePathBackup, ".info"); string fileNameBackup = $"{_ptc.CachePathBackup}.info";
FileInfo fileInfoActual = new FileInfo(fileNameActual); FileInfo fileInfoActual = new FileInfo(fileNameActual);

View File

@@ -81,7 +81,7 @@ namespace ARMeilleure.Translation
if (memory.Type.IsHostMapped()) if (memory.Type.IsHostMapped())
{ {
NativeSignalHandler.InitializeSignalHandler(); NativeSignalHandler.InitializeSignalHandler(allocator.GetPageSize());
} }
} }

View File

@@ -40,6 +40,12 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
info.InputBufferIndices[i] = (ushort)(bufferOffset + inputBufferOffset[i]); info.InputBufferIndices[i] = (ushort)(bufferOffset + inputBufferOffset[i]);
} }
if (info.BufferStates?.Length != (int)inputCount)
{
// Keep state if possible.
info.BufferStates = new UpsamplerBufferState[(int)inputCount];
}
UpsamplerInfo = info; UpsamplerInfo = info;
} }
@@ -50,8 +56,6 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
public void Process(CommandList context) public void Process(CommandList context)
{ {
float ratio = (float)InputSampleRate / Constants.TargetSampleRate;
uint bufferCount = Math.Min(BufferCount, UpsamplerInfo.SourceSampleCount); uint bufferCount = Math.Min(BufferCount, UpsamplerInfo.SourceSampleCount);
for (int i = 0; i < bufferCount; i++) for (int i = 0; i < bufferCount; i++)
@@ -59,9 +63,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
Span<float> inputBuffer = context.GetBuffer(UpsamplerInfo.InputBufferIndices[i]); Span<float> inputBuffer = context.GetBuffer(UpsamplerInfo.InputBufferIndices[i]);
Span<float> outputBuffer = GetBuffer(UpsamplerInfo.InputBufferIndices[i], (int)UpsamplerInfo.SampleCount); Span<float> outputBuffer = GetBuffer(UpsamplerInfo.InputBufferIndices[i], (int)UpsamplerInfo.SampleCount);
float fraction = 0.0f; UpsamplerHelper.Upsample(outputBuffer, inputBuffer, (int)UpsamplerInfo.SampleCount, (int)InputSampleCount, ref UpsamplerInfo.BufferStates[i]);
ResamplerHelper.ResampleForUpsampler(outputBuffer, inputBuffer, ratio, ref fraction, (int)(InputSampleCount / ratio));
} }
} }
} }

View File

@@ -579,52 +579,5 @@ namespace Ryujinx.Audio.Renderer.Dsp
fraction -= (int)fraction; fraction -= (int)fraction;
} }
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ResampleForUpsampler(Span<float> outputBuffer, ReadOnlySpan<float> inputBuffer, float ratio, ref float fraction, int sampleCount)
{
// Currently a simple cubic interpolation, assuming duplicated values at edges.
// TODO: Discover and use algorithm that the switch uses.
int inputBufferIndex = 0;
int maxIndex = inputBuffer.Length - 1;
int cubicEnd = inputBuffer.Length - 3;
for (int i = 0; i < sampleCount; i++)
{
float s0, s1, s2, s3;
s1 = inputBuffer[inputBufferIndex];
if (inputBufferIndex == 0 || inputBufferIndex > cubicEnd)
{
// Clamp interplation values at the ends of the input buffer.
s0 = inputBuffer[Math.Max(0, inputBufferIndex - 1)];
s2 = inputBuffer[Math.Min(maxIndex, inputBufferIndex + 1)];
s3 = inputBuffer[Math.Min(maxIndex, inputBufferIndex + 2)];
}
else
{
s0 = inputBuffer[inputBufferIndex - 1];
s2 = inputBuffer[inputBufferIndex + 1];
s3 = inputBuffer[inputBufferIndex + 2];
}
float a = s3 - s2 - s0 + s1;
float b = s0 - s1 - a;
float c = s2 - s0;
float d = s1;
float f2 = fraction * fraction;
float f3 = f2 * fraction;
outputBuffer[i] = a * f3 + b * f2 + c * fraction + d;
fraction += ratio;
inputBufferIndex += (int)MathF.Truncate(fraction);
fraction -= (int)fraction;
}
}
} }
} }

View File

@@ -0,0 +1,175 @@
using Ryujinx.Audio.Renderer.Server.Upsampler;
using Ryujinx.Common.Memory;
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace Ryujinx.Audio.Renderer.Dsp
{
public class UpsamplerHelper
{
private const int HistoryLength = UpsamplerBufferState.HistoryLength;
private const int FilterBankLength = 20;
// Bank0 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
private const int Bank0CenterIndex = 9;
private static readonly Array20<float> Bank1 = PrecomputeFilterBank(1.0f / 6.0f);
private static readonly Array20<float> Bank2 = PrecomputeFilterBank(2.0f / 6.0f);
private static readonly Array20<float> Bank3 = PrecomputeFilterBank(3.0f / 6.0f);
private static readonly Array20<float> Bank4 = PrecomputeFilterBank(4.0f / 6.0f);
private static readonly Array20<float> Bank5 = PrecomputeFilterBank(5.0f / 6.0f);
private static Array20<float> PrecomputeFilterBank(float offset)
{
float Sinc(float x)
{
if (x == 0)
{
return 1.0f;
}
return (MathF.Sin(MathF.PI * x) / (MathF.PI * x));
}
float BlackmanWindow(float x)
{
const float a = 0.18f;
const float a0 = 0.5f - 0.5f * a;
const float a1 = -0.5f;
const float a2 = 0.5f * a;
return a0 + a1 * MathF.Cos(2 * MathF.PI * x) + a2 * MathF.Cos(4 * MathF.PI * x);
}
Array20<float> result = new Array20<float>();
for (int i = 0; i < FilterBankLength; i++)
{
float x = (Bank0CenterIndex - i) + offset;
result[i] = Sinc(x) * BlackmanWindow(x / FilterBankLength + 0.5f);
}
return result;
}
// Polyphase upsampling algorithm
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Upsample(Span<float> outputBuffer, ReadOnlySpan<float> inputBuffer, int outputSampleCount, int inputSampleCount, ref UpsamplerBufferState state)
{
if (!state.Initialized)
{
state.Scale = inputSampleCount switch
{
40 => 6.0f,
80 => 3.0f,
160 => 1.5f,
_ => throw new ArgumentOutOfRangeException()
};
state.Initialized = true;
}
if (outputSampleCount == 0)
{
return;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
float DoFilterBank(ref UpsamplerBufferState state, in Array20<float> bank)
{
float result = 0.0f;
Debug.Assert(state.History.Length == HistoryLength);
Debug.Assert(bank.Length == FilterBankLength);
for (int j = 0; j < FilterBankLength; j++)
{
result += bank[j] * state.History[j];
}
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
void NextInput(ref UpsamplerBufferState state, float input)
{
state.History.AsSpan().Slice(1).CopyTo(state.History.AsSpan());
state.History[HistoryLength - 1] = input;
}
int inputBufferIndex = 0;
switch (state.Scale)
{
case 6.0f:
for (int i = 0; i < outputSampleCount; i++)
{
switch (state.Phase)
{
case 0:
NextInput(ref state, inputBuffer[inputBufferIndex++]);
outputBuffer[i] = state.History[Bank0CenterIndex];
break;
case 1:
outputBuffer[i] = DoFilterBank(ref state, Bank1);
break;
case 2:
outputBuffer[i] = DoFilterBank(ref state, Bank2);
break;
case 3:
outputBuffer[i] = DoFilterBank(ref state, Bank3);
break;
case 4:
outputBuffer[i] = DoFilterBank(ref state, Bank4);
break;
case 5:
outputBuffer[i] = DoFilterBank(ref state, Bank5);
break;
}
state.Phase = (state.Phase + 1) % 6;
}
break;
case 3.0f:
for (int i = 0; i < outputSampleCount; i++)
{
switch (state.Phase)
{
case 0:
NextInput(ref state, inputBuffer[inputBufferIndex++]);
outputBuffer[i] = state.History[Bank0CenterIndex];
break;
case 1:
outputBuffer[i] = DoFilterBank(ref state, Bank2);
break;
case 2:
outputBuffer[i] = DoFilterBank(ref state, Bank4);
break;
}
state.Phase = (state.Phase + 1) % 3;
}
break;
case 1.5f:
// Upsample by 3 then decimate by 2.
for (int i = 0; i < outputSampleCount; i++)
{
switch (state.Phase)
{
case 0:
NextInput(ref state, inputBuffer[inputBufferIndex++]);
outputBuffer[i] = state.History[Bank0CenterIndex];
break;
case 1:
outputBuffer[i] = DoFilterBank(ref state, Bank4);
break;
case 2:
NextInput(ref state, inputBuffer[inputBufferIndex++]);
outputBuffer[i] = DoFilterBank(ref state, Bank2);
break;
}
state.Phase = (state.Phase + 1) % 3;
}
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
}

View File

@@ -0,0 +1,14 @@
using Ryujinx.Common.Memory;
namespace Ryujinx.Audio.Renderer.Server.Upsampler
{
public struct UpsamplerBufferState
{
public const int HistoryLength = 20;
public float Scale;
public Array20<float> History;
public bool Initialized;
public int Phase;
}
}

View File

@@ -37,6 +37,11 @@ namespace Ryujinx.Audio.Renderer.Server.Upsampler
/// </summary> /// </summary>
public ushort[] InputBufferIndices; public ushort[] InputBufferIndices;
/// <summary>
/// State of each input buffer index kept across invocations of the upsampler.
/// </summary>
public UpsamplerBufferState[] BufferStates;
/// <summary> /// <summary>
/// Create a new <see cref="UpsamplerState"/>. /// Create a new <see cref="UpsamplerState"/>.
/// </summary> /// </summary>

View File

@@ -12,9 +12,9 @@ using Ryujinx.Audio.Integration;
using Ryujinx.Ava.Common; using Ryujinx.Ava.Common;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Input; using Ryujinx.Ava.Input;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.Renderer;
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common; using Ryujinx.Common;
@@ -46,6 +46,7 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop;
using Image = SixLabors.ImageSharp.Image; using Image = SixLabors.ImageSharp.Image;
using InputManager = Ryujinx.Input.HLE.InputManager; using InputManager = Ryujinx.Input.HLE.InputManager;
using Key = Ryujinx.Input.Key; using Key = Ryujinx.Input.Key;
@@ -58,12 +59,14 @@ namespace Ryujinx.Ava
{ {
internal class AppHost internal class AppHost
{ {
private const int CursorHideIdleTime = 8; // Hide Cursor seconds. private const int CursorHideIdleTime = 5; // Hide Cursor seconds.
private const float MaxResolutionScale = 4.0f; // Max resolution hotkeys can scale to before wrapping. private const float MaxResolutionScale = 4.0f; // Max resolution hotkeys can scale to before wrapping.
private const int TargetFps = 60; private const int TargetFps = 60;
private const float VolumeDelta = 0.05f; private const float VolumeDelta = 0.05f;
private static readonly Cursor InvisibleCursor = new(StandardCursorType.None); private static readonly Cursor InvisibleCursor = new(StandardCursorType.None);
private readonly IntPtr InvisibleCursorWin;
private readonly IntPtr DefaultCursorWin;
private readonly long _ticksPerFrame; private readonly long _ticksPerFrame;
private readonly Stopwatch _chrono; private readonly Stopwatch _chrono;
@@ -76,12 +79,12 @@ namespace Ryujinx.Ava
private readonly MainWindowViewModel _viewModel; private readonly MainWindowViewModel _viewModel;
private readonly IKeyboard _keyboardInterface; private readonly IKeyboard _keyboardInterface;
private readonly TopLevel _topLevel; private readonly TopLevel _topLevel;
public RendererHost _rendererHost;
private readonly GraphicsDebugLevel _glLogLevel; private readonly GraphicsDebugLevel _glLogLevel;
private float _newVolume; private float _newVolume;
private KeyboardHotkeyState _prevHotkeyState; private KeyboardHotkeyState _prevHotkeyState;
private bool _hideCursorOnIdle;
private long _lastCursorMoveTime; private long _lastCursorMoveTime;
private bool _isCursorInRenderer; private bool _isCursorInRenderer;
@@ -102,7 +105,6 @@ namespace Ryujinx.Ava
public event EventHandler AppExit; public event EventHandler AppExit;
public event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent; public event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent;
public RendererHost Renderer { get; }
public VirtualFileSystem VirtualFileSystem { get; } public VirtualFileSystem VirtualFileSystem { get; }
public ContentManager ContentManager { get; } public ContentManager ContentManager { get; }
public NpadManager NpadManager { get; } public NpadManager NpadManager { get; }
@@ -114,7 +116,6 @@ namespace Ryujinx.Ava
public string ApplicationPath { get; private set; } public string ApplicationPath { get; private set; }
public bool ScreenshotRequested { get; set; } public bool ScreenshotRequested { get; set; }
public AppHost( public AppHost(
RendererHost renderer, RendererHost renderer,
InputManager inputManager, InputManager inputManager,
@@ -131,7 +132,6 @@ namespace Ryujinx.Ava
_accountManager = accountManager; _accountManager = accountManager;
_userChannelPersistence = userChannelPersistence; _userChannelPersistence = userChannelPersistence;
_renderingThread = new Thread(RenderLoop, 1 * 1024 * 1024) { Name = "GUI.RenderThread" }; _renderingThread = new Thread(RenderLoop, 1 * 1024 * 1024) { Name = "GUI.RenderThread" };
_hideCursorOnIdle = ConfigurationState.Instance.HideCursorOnIdle;
_lastCursorMoveTime = Stopwatch.GetTimestamp(); _lastCursorMoveTime = Stopwatch.GetTimestamp();
_glLogLevel = ConfigurationState.Instance.Logger.GraphicsDebugLevel; _glLogLevel = ConfigurationState.Instance.Logger.GraphicsDebugLevel;
_topLevel = topLevel; _topLevel = topLevel;
@@ -142,11 +142,12 @@ namespace Ryujinx.Ava
NpadManager = _inputManager.CreateNpadManager(); NpadManager = _inputManager.CreateNpadManager();
TouchScreenManager = _inputManager.CreateTouchScreenManager(); TouchScreenManager = _inputManager.CreateTouchScreenManager();
Renderer = renderer;
ApplicationPath = applicationPath; ApplicationPath = applicationPath;
VirtualFileSystem = virtualFileSystem; VirtualFileSystem = virtualFileSystem;
ContentManager = contentManager; ContentManager = contentManager;
_rendererHost = renderer;
_chrono = new Stopwatch(); _chrono = new Stopwatch();
_ticksPerFrame = Stopwatch.Frequency / TargetFps; _ticksPerFrame = Stopwatch.Frequency / TargetFps;
@@ -159,9 +160,14 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.HideCursorOnIdle.Event += HideCursorState_Changed; ConfigurationState.Instance.HideCursorOnIdle.Event += HideCursorState_Changed;
_topLevel.PointerLeave += TopLevel_PointerLeave;
_topLevel.PointerMoved += TopLevel_PointerMoved; _topLevel.PointerMoved += TopLevel_PointerMoved;
if (OperatingSystem.IsWindows())
{
InvisibleCursorWin = CreateEmptyCursor();
DefaultCursorWin = CreateArrowCursor();
}
ConfigurationState.Instance.System.IgnoreMissingServices.Event += UpdateIgnoreMissingServicesState; ConfigurationState.Instance.System.IgnoreMissingServices.Event += UpdateIgnoreMissingServicesState;
ConfigurationState.Instance.Graphics.AspectRatio.Event += UpdateAspectRatioState; ConfigurationState.Instance.Graphics.AspectRatio.Event += UpdateAspectRatioState;
ConfigurationState.Instance.System.EnableDockedMode.Event += UpdateDockedModeState; ConfigurationState.Instance.System.EnableDockedMode.Event += UpdateDockedModeState;
@@ -172,20 +178,47 @@ namespace Ryujinx.Ava
private void TopLevel_PointerMoved(object sender, PointerEventArgs e) private void TopLevel_PointerMoved(object sender, PointerEventArgs e)
{ {
if (sender is Control visual) if (sender is MainWindow window)
{ {
_lastCursorMoveTime = Stopwatch.GetTimestamp(); _lastCursorMoveTime = Stopwatch.GetTimestamp();
var point = e.GetCurrentPoint(visual).Position; if (_rendererHost.EmbeddedWindow.TransformedBounds != null)
{
_isCursorInRenderer = Equals(visual.InputHitTest(point), Renderer); var point = e.GetCurrentPoint(window).Position;
} var bounds = _rendererHost.EmbeddedWindow.TransformedBounds.Value.Clip;
}
_isCursorInRenderer = point.X >= bounds.X &&
private void TopLevel_PointerLeave(object sender, PointerEventArgs e) point.X <= bounds.Width + bounds.X &&
point.Y >= bounds.Y &&
point.Y <= bounds.Height + bounds.Y;
}
}
}
private void ShowCursor()
{
Dispatcher.UIThread.Post(() =>
{ {
_isCursorInRenderer = false;
_viewModel.Cursor = Cursor.Default; _viewModel.Cursor = Cursor.Default;
if (OperatingSystem.IsWindows())
{
SetCursor(DefaultCursorWin);
}
});
}
private void HideCursor()
{
Dispatcher.UIThread.Post(() =>
{
_viewModel.Cursor = InvisibleCursor;
if (OperatingSystem.IsWindows())
{
SetCursor(InvisibleCursorWin);
}
});
} }
private void SetRendererWindowSize(Size size) private void SetRendererWindowSize(Size size)
@@ -284,7 +317,7 @@ namespace Ryujinx.Ava
_viewModel.SetUIProgressHandlers(Device); _viewModel.SetUIProgressHandlers(Device);
Renderer.SizeChanged += Window_SizeChanged; _rendererHost.SizeChanged += Window_SizeChanged;
_isActive = true; _isActive = true;
@@ -380,7 +413,6 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.System.EnableDockedMode.Event -= UpdateDockedModeState; ConfigurationState.Instance.System.EnableDockedMode.Event -= UpdateDockedModeState;
ConfigurationState.Instance.System.AudioVolume.Event -= UpdateAudioVolumeState; ConfigurationState.Instance.System.AudioVolume.Event -= UpdateAudioVolumeState;
_topLevel.PointerLeave -= TopLevel_PointerLeave;
_topLevel.PointerMoved -= TopLevel_PointerMoved; _topLevel.PointerMoved -= TopLevel_PointerMoved;
_gpuCancellationTokenSource.Cancel(); _gpuCancellationTokenSource.Cancel();
@@ -397,28 +429,19 @@ namespace Ryujinx.Ava
_windowsMultimediaTimerResolution = null; _windowsMultimediaTimerResolution = null;
} }
Renderer?.MakeCurrent(); (_rendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.MakeCurrent();
Device.DisposeGpu(); Device.DisposeGpu();
Renderer?.MakeCurrent(null); (_rendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.MakeCurrent(null);
} }
private void HideCursorState_Changed(object sender, ReactiveEventArgs<bool> state) private void HideCursorState_Changed(object sender, ReactiveEventArgs<bool> state)
{ {
Dispatcher.UIThread.InvokeAsync(delegate if (state.NewValue)
{
_hideCursorOnIdle = state.NewValue;
if (_hideCursorOnIdle)
{ {
_lastCursorMoveTime = Stopwatch.GetTimestamp(); _lastCursorMoveTime = Stopwatch.GetTimestamp();
} }
else
{
_viewModel.Cursor = Cursor.Default;
}
});
} }
public async Task<bool> LoadGuestApplication() public async Task<bool> LoadGuestApplication()
@@ -611,11 +634,12 @@ namespace Ryujinx.Ava
// Initialize Renderer. // Initialize Renderer.
IRenderer renderer; IRenderer renderer;
if (Renderer.IsVulkan) if (ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.Vulkan)
{ {
string preferredGpu = ConfigurationState.Instance.Graphics.PreferredGpu.Value; renderer = new VulkanRenderer(
(_rendererHost.EmbeddedWindow as EmbeddedWindowVulkan).CreateSurface,
renderer = new VulkanRenderer(Renderer.CreateVulkanSurface, VulkanHelper.GetRequiredInstanceExtensions, preferredGpu); VulkanHelper.GetRequiredInstanceExtensions,
ConfigurationState.Instance.Graphics.PreferredGpu.Value);
} }
else else
{ {
@@ -763,14 +787,12 @@ namespace Ryujinx.Ava
_renderer.ScreenCaptured += Renderer_ScreenCaptured; _renderer.ScreenCaptured += Renderer_ScreenCaptured;
(_renderer as OpenGLRenderer)?.InitializeBackgroundContext(SPBOpenGLContext.CreateBackgroundContext(Renderer.GetContext())); (_rendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.InitializeBackgroundContext(_renderer);
Renderer.MakeCurrent();
Device.Gpu.Renderer.Initialize(_glLogLevel); Device.Gpu.Renderer.Initialize(_glLogLevel);
Width = (int)Renderer.Bounds.Width; Width = (int)_rendererHost.Bounds.Width;
Height = (int)Renderer.Bounds.Height; Height = (int)_rendererHost.Bounds.Height;
_renderer.Window.SetSize((int)(Width * _topLevel.PlatformImpl.RenderScaling), (int)(Height * _topLevel.PlatformImpl.RenderScaling)); _renderer.Window.SetSize((int)(Width * _topLevel.PlatformImpl.RenderScaling), (int)(Height * _topLevel.PlatformImpl.RenderScaling));
@@ -803,7 +825,7 @@ namespace Ryujinx.Ava
_viewModel.SwitchToRenderer(false); _viewModel.SwitchToRenderer(false);
} }
Device.PresentFrame(() => Renderer?.SwapBuffers()); Device.PresentFrame(() => (_rendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.SwapBuffers());
} }
if (_ticks >= _ticksPerFrame) if (_ticks >= _ticksPerFrame)
@@ -813,7 +835,7 @@ namespace Ryujinx.Ava
} }
}); });
Renderer?.MakeCurrent(null); (_rendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.MakeCurrent(null);
} }
public void UpdateStatus() public void UpdateStatus()
@@ -829,7 +851,7 @@ namespace Ryujinx.Ava
StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs( StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs(
Device.EnableDeviceVsync, Device.EnableDeviceVsync,
LocaleManager.Instance[LocaleKeys.VolumeShort] + $": {(int)(Device.GetVolume() * 100)}%", LocaleManager.Instance[LocaleKeys.VolumeShort] + $": {(int)(Device.GetVolume() * 100)}%",
Renderer.IsVulkan ? "Vulkan" : "OpenGL", ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.Vulkan ? "Vulkan" : "OpenGL",
dockedMode, dockedMode,
ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(), ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(),
LocaleManager.Instance[LocaleKeys.Game] + $": {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)", LocaleManager.Instance[LocaleKeys.Game] + $": {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)",
@@ -860,29 +882,6 @@ namespace Ryujinx.Ava
} }
} }
private void HandleScreenState()
{
if (ConfigurationState.Instance.Hid.EnableMouse)
{
Dispatcher.UIThread.Post(() =>
{
_viewModel.Cursor = _isCursorInRenderer ? InvisibleCursor : Cursor.Default;
});
}
else
{
if (_hideCursorOnIdle)
{
long cursorMoveDelta = Stopwatch.GetTimestamp() - _lastCursorMoveTime;
Dispatcher.UIThread.Post(() =>
{
_viewModel.Cursor = cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency ? InvisibleCursor : Cursor.Default;
});
}
}
}
private bool UpdateFrame() private bool UpdateFrame()
{ {
if (!_isActive) if (!_isActive)
@@ -890,23 +889,44 @@ namespace Ryujinx.Ava
return false; return false;
} }
NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
if (_viewModel.IsActive) if (_viewModel.IsActive)
{ {
if (ConfigurationState.Instance.Hid.EnableMouse)
{
if (_isCursorInRenderer)
{
HideCursor();
}
else
{
ShowCursor();
}
}
else
{
if (ConfigurationState.Instance.HideCursorOnIdle)
{
if (Stopwatch.GetTimestamp() - _lastCursorMoveTime >= CursorHideIdleTime * Stopwatch.Frequency)
{
HideCursor();
}
else
{
ShowCursor();
}
}
}
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
{ {
HandleScreenState();
if (_keyboardInterface.GetKeyboardStateSnapshot().IsPressed(Key.Delete) && _viewModel.WindowState != WindowState.FullScreen) if (_keyboardInterface.GetKeyboardStateSnapshot().IsPressed(Key.Delete) && _viewModel.WindowState != WindowState.FullScreen)
{ {
Device.Application.DiskCacheLoadState?.Cancel(); Device.Application.DiskCacheLoadState?.Cancel();
} }
}); });
}
NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
if (_viewModel.IsActive)
{
KeyboardHotkeyState currentHotkeyState = GetHotkeyState(); KeyboardHotkeyState currentHotkeyState = GetHotkeyState();
if (currentHotkeyState != _prevHotkeyState) if (currentHotkeyState != _prevHotkeyState)

View File

@@ -260,7 +260,7 @@
"UserProfilesChangeProfileImage": "Profilbild ändern", "UserProfilesChangeProfileImage": "Profilbild ändern",
"UserProfilesAvailableUserProfiles": "Verfügbare Profile:", "UserProfilesAvailableUserProfiles": "Verfügbare Profile:",
"UserProfilesAddNewProfile": "Neues Profil", "UserProfilesAddNewProfile": "Neues Profil",
"UserProfilesDeleteSelectedProfile": "Profil löschen", "UserProfilesDelete": "Löschen",
"UserProfilesClose": "Schließen", "UserProfilesClose": "Schließen",
"ProfileImageSelectionTitle": "Auswahl des Profilbildes", "ProfileImageSelectionTitle": "Auswahl des Profilbildes",
"ProfileImageSelectionHeader": "Wähle ein Profilbild aus", "ProfileImageSelectionHeader": "Wähle ein Profilbild aus",

View File

@@ -260,7 +260,7 @@
"UserProfilesChangeProfileImage": "Αλλαγή Εικόνας Προφίλ", "UserProfilesChangeProfileImage": "Αλλαγή Εικόνας Προφίλ",
"UserProfilesAvailableUserProfiles": "Διαθέσιμα Προφίλ Χρηστών:", "UserProfilesAvailableUserProfiles": "Διαθέσιμα Προφίλ Χρηστών:",
"UserProfilesAddNewProfile": "Προσθήκη Νέου Προφίλ", "UserProfilesAddNewProfile": "Προσθήκη Νέου Προφίλ",
"UserProfilesDeleteSelectedProfile": "Διαγραφή Επιλεγμένου Προφίλ", "UserProfilesDelete": "Διαγράφω",
"UserProfilesClose": "Κλείσιμο", "UserProfilesClose": "Κλείσιμο",
"ProfileImageSelectionTitle": "Επιλογή Εικόνας Προφίλ", "ProfileImageSelectionTitle": "Επιλογή Εικόνας Προφίλ",
"ProfileImageSelectionHeader": "Επιλέξτε μία Εικόνα Προφίλ", "ProfileImageSelectionHeader": "Επιλέξτε μία Εικόνα Προφίλ",

View File

@@ -119,7 +119,7 @@
"SettingsTabSystemAudioBackendSoundIO": "SoundIO", "SettingsTabSystemAudioBackendSoundIO": "SoundIO",
"SettingsTabSystemAudioBackendSDL2": "SDL2", "SettingsTabSystemAudioBackendSDL2": "SDL2",
"SettingsTabSystemHacks": "Hacks", "SettingsTabSystemHacks": "Hacks",
"SettingsTabSystemHacksNote": " (may cause instability)", "SettingsTabSystemHacksNote": "May cause instability",
"SettingsTabSystemExpandDramSize": "Use alternative memory layout (Developers)", "SettingsTabSystemExpandDramSize": "Use alternative memory layout (Developers)",
"SettingsTabSystemIgnoreMissingServices": "Ignore Missing Services", "SettingsTabSystemIgnoreMissingServices": "Ignore Missing Services",
"SettingsTabGraphics": "Graphics", "SettingsTabGraphics": "Graphics",
@@ -157,7 +157,8 @@
"SettingsTabLoggingEnableGuestLogs": "Enable Guest Logs", "SettingsTabLoggingEnableGuestLogs": "Enable Guest Logs",
"SettingsTabLoggingEnableFsAccessLogs": "Enable Fs Access Logs", "SettingsTabLoggingEnableFsAccessLogs": "Enable Fs Access Logs",
"SettingsTabLoggingFsGlobalAccessLogMode": "Fs Global Access Log Mode:", "SettingsTabLoggingFsGlobalAccessLogMode": "Fs Global Access Log Mode:",
"SettingsTabLoggingDeveloperOptions": "Developer Options (WARNING: Will reduce performance)", "SettingsTabLoggingDeveloperOptions": "Developer Options",
"SettingsTabLoggingDeveloperOptionsNote": "WARNING: Will reduce performance",
"SettingsTabLoggingGraphicsBackendLogLevel": "Graphics Backend Log Level:", "SettingsTabLoggingGraphicsBackendLogLevel": "Graphics Backend Log Level:",
"SettingsTabLoggingGraphicsBackendLogLevelNone": "None", "SettingsTabLoggingGraphicsBackendLogLevelNone": "None",
"SettingsTabLoggingGraphicsBackendLogLevelError": "Error", "SettingsTabLoggingGraphicsBackendLogLevelError": "Error",
@@ -260,8 +261,9 @@
"UserProfilesChangeProfileImage": "Change Profile Image", "UserProfilesChangeProfileImage": "Change Profile Image",
"UserProfilesAvailableUserProfiles": "Available User Profiles:", "UserProfilesAvailableUserProfiles": "Available User Profiles:",
"UserProfilesAddNewProfile": "Create Profile", "UserProfilesAddNewProfile": "Create Profile",
"UserProfilesDeleteSelectedProfile": "Delete Selected", "UserProfilesDelete": "Delete",
"UserProfilesClose": "Close", "UserProfilesClose": "Close",
"ProfileNameSelectionWatermark": "Choose a nickname",
"ProfileImageSelectionTitle": "Profile Image Selection", "ProfileImageSelectionTitle": "Profile Image Selection",
"ProfileImageSelectionHeader": "Choose a profile Image", "ProfileImageSelectionHeader": "Choose a profile Image",
"ProfileImageSelectionNote": "You may import a custom profile image, or select an avatar from system firmware", "ProfileImageSelectionNote": "You may import a custom profile image, or select an avatar from system firmware",
@@ -273,7 +275,7 @@
"InputDialogAddNewProfileTitle": "Choose the Profile Name", "InputDialogAddNewProfileTitle": "Choose the Profile Name",
"InputDialogAddNewProfileHeader": "Please Enter a Profile Name", "InputDialogAddNewProfileHeader": "Please Enter a Profile Name",
"InputDialogAddNewProfileSubtext": "(Max Length: {0})", "InputDialogAddNewProfileSubtext": "(Max Length: {0})",
"AvatarChoose": "Choose", "AvatarChoose": "Choose Avatar",
"AvatarSetBackgroundColor": "Set Background Color", "AvatarSetBackgroundColor": "Set Background Color",
"AvatarClose": "Close", "AvatarClose": "Close",
"ControllerSettingsLoadProfileToolTip": "Load Profile", "ControllerSettingsLoadProfileToolTip": "Load Profile",
@@ -368,6 +370,9 @@
"DialogFirmwareInstallerFirmwareInstallSuccessMessage": "System version {0} successfully installed.", "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "System version {0} successfully installed.",
"DialogUserProfileDeletionWarningMessage": "There would be no other profiles to be opened if selected profile is deleted", "DialogUserProfileDeletionWarningMessage": "There would be no other profiles to be opened if selected profile is deleted",
"DialogUserProfileDeletionConfirmMessage": "Do you want to delete the selected profile", "DialogUserProfileDeletionConfirmMessage": "Do you want to delete the selected profile",
"DialogUserProfileUnsavedChangesTitle": "Warning - Unsaved Changes",
"DialogUserProfileUnsavedChangesMessage": "You have made changes to this user profile that have not been saved.",
"DialogUserProfileUnsavedChangesSubMessage": "Do you want to discard your changes?",
"DialogControllerSettingsModifiedConfirmMessage": "The current controller settings has been updated.", "DialogControllerSettingsModifiedConfirmMessage": "The current controller settings has been updated.",
"DialogControllerSettingsModifiedConfirmSubMessage": "Do you want to save?", "DialogControllerSettingsModifiedConfirmSubMessage": "Do you want to save?",
"DialogDlcLoadNcaErrorMessage": "{0}. Errored File: {1}", "DialogDlcLoadNcaErrorMessage": "{0}. Errored File: {1}",
@@ -519,7 +524,7 @@
"UserErrorUndefinedDescription": "An undefined error occured! This shouldn't happen, please contact a dev!", "UserErrorUndefinedDescription": "An undefined error occured! This shouldn't happen, please contact a dev!",
"OpenSetupGuideMessage": "Open the Setup Guide", "OpenSetupGuideMessage": "Open the Setup Guide",
"NoUpdate": "No Update", "NoUpdate": "No Update",
"TitleUpdateVersionLabel": "Version {0} - {1}", "TitleUpdateVersionLabel": "Version {0}",
"RyujinxInfo": "Ryujinx - Info", "RyujinxInfo": "Ryujinx - Info",
"RyujinxConfirm": "Ryujinx - Confirmation", "RyujinxConfirm": "Ryujinx - Confirmation",
"FileDialogAllTypes": "All types", "FileDialogAllTypes": "All types",
@@ -580,11 +585,11 @@
"UserProfilesSetProfileImage": "Set Profile Image", "UserProfilesSetProfileImage": "Set Profile Image",
"UserProfileEmptyNameError": "Name is required", "UserProfileEmptyNameError": "Name is required",
"UserProfileNoImageError": "Profile image must be set", "UserProfileNoImageError": "Profile image must be set",
"GameUpdateWindowHeading": "{0} Update(s) available for {1} ({2})", "GameUpdateWindowHeading": "Manage Updates for {0} ({1})",
"SettingsTabHotkeysResScaleUpHotkey": "Increase resolution:", "SettingsTabHotkeysResScaleUpHotkey": "Increase resolution:",
"SettingsTabHotkeysResScaleDownHotkey": "Decrease resolution:", "SettingsTabHotkeysResScaleDownHotkey": "Decrease resolution:",
"UserProfilesName": "Name:", "UserProfilesName": "Name:",
"UserProfilesUserId": "User Id:", "UserProfilesUserId": "User ID:",
"SettingsTabGraphicsBackend": "Graphics Backend", "SettingsTabGraphicsBackend": "Graphics Backend",
"SettingsTabGraphicsBackendTooltip": "Graphics Backend to use", "SettingsTabGraphicsBackendTooltip": "Graphics Backend to use",
"SettingsEnableTextureRecompression": "Enable Texture Recompression", "SettingsEnableTextureRecompression": "Enable Texture Recompression",
@@ -603,13 +608,15 @@
"UserProfilesManageSaves": "Manage Saves", "UserProfilesManageSaves": "Manage Saves",
"DeleteUserSave": "Do you want to delete user save for this game?", "DeleteUserSave": "Do you want to delete user save for this game?",
"IrreversibleActionNote": "This action is not reversible.", "IrreversibleActionNote": "This action is not reversible.",
"SaveManagerHeading": "Manage Saves for {0}", "SaveManagerHeading": "Manage Saves for {0} ({1})",
"SaveManagerTitle": "Save Manager", "SaveManagerTitle": "Save Manager",
"Name": "Name", "Name": "Name",
"Size": "Size", "Size": "Size",
"Search": "Search", "Search": "Search",
"UserProfilesRecoverLostAccounts": "Recover Lost Accounts", "UserProfilesRecoverLostAccounts": "Recover Lost Accounts",
"Recover": "Recover", "Recover": "Recover",
"UserProfilesRecoverHeading" : "Saves were found for the following accounts" "UserProfilesRecoverHeading" : "Saves were found for the following accounts",
"UserProfilesRecoverEmptyList": "No profiles to recover",
"UserEditorTitle" : "Edit User",
"UserEditorTitleCreate" : "Create User"
} }

View File

@@ -260,7 +260,7 @@
"UserProfilesChangeProfileImage": "Cambiar imagen de perfil", "UserProfilesChangeProfileImage": "Cambiar imagen de perfil",
"UserProfilesAvailableUserProfiles": "Perfiles de usuario disponibles:", "UserProfilesAvailableUserProfiles": "Perfiles de usuario disponibles:",
"UserProfilesAddNewProfile": "Añadir nuevo perfil", "UserProfilesAddNewProfile": "Añadir nuevo perfil",
"UserProfilesDeleteSelectedProfile": "Eliminar perfil seleccionado", "UserProfilesDelete": "Eliminar",
"UserProfilesClose": "Cerrar", "UserProfilesClose": "Cerrar",
"ProfileImageSelectionTitle": "Selección de imagen de perfil", "ProfileImageSelectionTitle": "Selección de imagen de perfil",
"ProfileImageSelectionHeader": "Elige una imagen de perfil", "ProfileImageSelectionHeader": "Elige una imagen de perfil",

View File

@@ -260,7 +260,7 @@
"UserProfilesChangeProfileImage": "Changer l'image du profil", "UserProfilesChangeProfileImage": "Changer l'image du profil",
"UserProfilesAvailableUserProfiles": "Profils utilisateurs disponible:", "UserProfilesAvailableUserProfiles": "Profils utilisateurs disponible:",
"UserProfilesAddNewProfile": "Ajouter un nouveau profil", "UserProfilesAddNewProfile": "Ajouter un nouveau profil",
"UserProfilesDeleteSelectedProfile": "Supprimer le profil sélectionné", "UserProfilesDelete": "Supprimer",
"UserProfilesClose": "Fermer", "UserProfilesClose": "Fermer",
"ProfileImageSelectionTitle": "Sélection de l'image du profil", "ProfileImageSelectionTitle": "Sélection de l'image du profil",
"ProfileImageSelectionHeader": "Choisir l'image du profil", "ProfileImageSelectionHeader": "Choisir l'image du profil",

View File

@@ -260,7 +260,7 @@
"UserProfilesChangeProfileImage": "プロファイル画像を変更", "UserProfilesChangeProfileImage": "プロファイル画像を変更",
"UserProfilesAvailableUserProfiles": "利用可能なユーザプロファイル:", "UserProfilesAvailableUserProfiles": "利用可能なユーザプロファイル:",
"UserProfilesAddNewProfile": "プロファイルを作成", "UserProfilesAddNewProfile": "プロファイルを作成",
"UserProfilesDeleteSelectedProfile": "削除", "UserProfilesDelete": "削除",
"UserProfilesClose": "閉じる", "UserProfilesClose": "閉じる",
"ProfileImageSelectionTitle": "プロファイル画像選択", "ProfileImageSelectionTitle": "プロファイル画像選択",
"ProfileImageSelectionHeader": "プロファイル画像を選択", "ProfileImageSelectionHeader": "プロファイル画像を選択",

View File

@@ -260,7 +260,7 @@
"UserProfilesChangeProfileImage": "Zmień Obraz Profilu", "UserProfilesChangeProfileImage": "Zmień Obraz Profilu",
"UserProfilesAvailableUserProfiles": "Dostępne Profile Użytkowników:", "UserProfilesAvailableUserProfiles": "Dostępne Profile Użytkowników:",
"UserProfilesAddNewProfile": "Utwórz Profil", "UserProfilesAddNewProfile": "Utwórz Profil",
"UserProfilesDeleteSelectedProfile": "Usuń Zaznaczone", "UserProfilesDelete": "Usuwać",
"UserProfilesClose": "Zamknij", "UserProfilesClose": "Zamknij",
"ProfileImageSelectionTitle": "Wybór Obrazu Profilu", "ProfileImageSelectionTitle": "Wybór Obrazu Profilu",
"ProfileImageSelectionHeader": "Wybierz zdjęcie profilowe", "ProfileImageSelectionHeader": "Wybierz zdjęcie profilowe",

View File

@@ -260,7 +260,7 @@
"UserProfilesChangeProfileImage": "Mudar imagem de perfil", "UserProfilesChangeProfileImage": "Mudar imagem de perfil",
"UserProfilesAvailableUserProfiles": "Perfis de usuário disponíveis:", "UserProfilesAvailableUserProfiles": "Perfis de usuário disponíveis:",
"UserProfilesAddNewProfile": "Adicionar novo perfil", "UserProfilesAddNewProfile": "Adicionar novo perfil",
"UserProfilesDeleteSelectedProfile": "Apagar perfil selecionado", "UserProfilesDelete": "Apagar",
"UserProfilesClose": "Fechar", "UserProfilesClose": "Fechar",
"ProfileImageSelectionTitle": "Seleção da imagem de perfil", "ProfileImageSelectionTitle": "Seleção da imagem de perfil",
"ProfileImageSelectionHeader": "Escolha uma imagem de perfil", "ProfileImageSelectionHeader": "Escolha uma imagem de perfil",

View File

@@ -260,7 +260,7 @@
"UserProfilesChangeProfileImage": "Изменить изображение профиля", "UserProfilesChangeProfileImage": "Изменить изображение профиля",
"UserProfilesAvailableUserProfiles": "Доступные профили пользователей:", "UserProfilesAvailableUserProfiles": "Доступные профили пользователей:",
"UserProfilesAddNewProfile": "Добавить новый профиль", "UserProfilesAddNewProfile": "Добавить новый профиль",
"UserProfilesDeleteSelectedProfile": "Удалить выбранный профиль", "UserProfilesDelete": "Удалить",
"UserProfilesClose": "Закрыть", "UserProfilesClose": "Закрыть",
"ProfileImageSelectionTitle": "Выбор изображения профиля", "ProfileImageSelectionTitle": "Выбор изображения профиля",
"ProfileImageSelectionHeader": "Выберите изображение профиля", "ProfileImageSelectionHeader": "Выберите изображение профиля",

View File

@@ -260,7 +260,7 @@
"UserProfilesChangeProfileImage": "Profil Resmini Değiştir", "UserProfilesChangeProfileImage": "Profil Resmini Değiştir",
"UserProfilesAvailableUserProfiles": "Mevcut Kullanıcı Profilleri:", "UserProfilesAvailableUserProfiles": "Mevcut Kullanıcı Profilleri:",
"UserProfilesAddNewProfile": "Yeni Profil Ekle", "UserProfilesAddNewProfile": "Yeni Profil Ekle",
"UserProfilesDeleteSelectedProfile": "Seçili Profili Sil", "UserProfilesDelete": "Sil",
"UserProfilesClose": "Kapat", "UserProfilesClose": "Kapat",
"ProfileImageSelectionTitle": "Profil Resmi Seçimi", "ProfileImageSelectionTitle": "Profil Resmi Seçimi",
"ProfileImageSelectionHeader": "Profil Resmi Seç", "ProfileImageSelectionHeader": "Profil Resmi Seç",

View File

@@ -260,7 +260,7 @@
"UserProfilesChangeProfileImage": "更換頭貼", "UserProfilesChangeProfileImage": "更換頭貼",
"UserProfilesAvailableUserProfiles": "現有的帳號:", "UserProfilesAvailableUserProfiles": "現有的帳號:",
"UserProfilesAddNewProfile": "建立帳號", "UserProfilesAddNewProfile": "建立帳號",
"UserProfilesDeleteSelectedProfile": "刪除選擇的帳號", "UserProfilesDelete": "刪除",
"UserProfilesClose": "關閉", "UserProfilesClose": "關閉",
"ProfileImageSelectionTitle": "頭貼選擇", "ProfileImageSelectionTitle": "頭貼選擇",
"ProfileImageSelectionHeader": "選擇合適的頭貼圖片", "ProfileImageSelectionHeader": "選擇合適的頭貼圖片",

View File

@@ -60,5 +60,6 @@
<Color x:Key="MenuFlyoutPresenterBorderColor">#3D3D3D</Color> <Color x:Key="MenuFlyoutPresenterBorderColor">#3D3D3D</Color>
<Color x:Key="AppListBackgroundColor">#0FFFFFFF</Color> <Color x:Key="AppListBackgroundColor">#0FFFFFFF</Color>
<Color x:Key="AppListHoverBackgroundColor">#1EFFFFFF</Color> <Color x:Key="AppListHoverBackgroundColor">#1EFFFFFF</Color>
<Color x:Key="SecondaryTextColor">#A0FFFFFF</Color>
</Styles.Resources> </Styles.Resources>
</Styles> </Styles>

View File

@@ -52,5 +52,6 @@
<Color x:Key="MenuFlyoutPresenterBorderColor">#C1C1C1</Color> <Color x:Key="MenuFlyoutPresenterBorderColor">#C1C1C1</Color>
<Color x:Key="AppListBackgroundColor">#b3ffffff</Color> <Color x:Key="AppListBackgroundColor">#b3ffffff</Color>
<Color x:Key="AppListHoverBackgroundColor">#80cccccc</Color> <Color x:Key="AppListHoverBackgroundColor">#80cccccc</Color>
<Color x:Key="SecondaryTextColor">#A0000000</Color>
</Styles.Resources> </Styles.Resources>
</Styles> </Styles>

View File

@@ -56,8 +56,8 @@
<Style Selector="Border.settings"> <Style Selector="Border.settings">
<Setter Property="Background" Value="{DynamicResource ThemeDarkColor}" /> <Setter Property="Background" Value="{DynamicResource ThemeDarkColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource MenuFlyoutPresenterBorderColor}" /> <Setter Property="BorderBrush" Value="{DynamicResource MenuFlyoutPresenterBorderColor}" />
<Setter Property="BorderThickness" Value="2" /> <Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="3" /> <Setter Property="CornerRadius" Value="5" />
</Style> </Style>
<Style Selector="Image.small"> <Style Selector="Image.small">
<Setter Property="Width" Value="50" /> <Setter Property="Width" Value="50" />
@@ -179,6 +179,9 @@
<Style Selector="Button"> <Style Selector="Button">
<Setter Property="MinWidth" Value="80" /> <Setter Property="MinWidth" Value="80" />
</Style> </Style>
<Style Selector="ProgressBar /template/ Border#ProgressBarTrack">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="ToggleButton"> <Style Selector="ToggleButton">
<Setter Property="Padding" Value="0,-5,0,0" /> <Setter Property="Padding" Value="0,-5,0,0" />
</Style> </Style>
@@ -231,9 +234,41 @@
<Setter Property="BorderBrush" Value="{DynamicResource MenuFlyoutPresenterBorderBrush}" /> <Setter Property="BorderBrush" Value="{DynamicResource MenuFlyoutPresenterBorderBrush}" />
<Setter Property="BorderThickness" Value="{DynamicResource MenuFlyoutPresenterBorderThemeThickness}" /> <Setter Property="BorderThickness" Value="{DynamicResource MenuFlyoutPresenterBorderThemeThickness}" />
</Style> </Style>
<Style Selector="TextBox">
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<Style Selector="TextBox.NumberBoxTextBoxStyle"> <Style Selector="TextBox.NumberBoxTextBoxStyle">
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundColor}" /> <Setter Property="Foreground" Value="{DynamicResource ThemeForegroundColor}" />
</Style> </Style>
<Style Selector="ListBox ListBoxItem">
<Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="0" />
<Setter Property="CornerRadius" Value="5" />
<Setter Property="Background" Value="{DynamicResource AppListBackgroundColor}" />
<Setter Property="BorderThickness" Value="2"/>
<Style.Animations>
<Animation Duration="0:0:0.7">
<KeyFrame Cue="0%">
<Setter Property="MaxHeight" Value="0" />
<Setter Property="Opacity" Value="0.0" />
</KeyFrame>
<KeyFrame Cue="50%">
<Setter Property="MaxHeight" Value="1000" />
<Setter Property="Opacity" Value="0.3" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="MaxHeight" Value="1000" />
<Setter Property="Opacity" Value="1.0" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="ListBox ListBoxItem:selected /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AppListBackgroundColor}" />
</Style>
<Style Selector="ListBox ListBoxItem:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AppListHoverBackgroundColor}" />
</Style>
<Styles.Resources> <Styles.Resources>
<SolidColorBrush x:Key="ThemeAccentColorBrush" Color="{DynamicResource SystemAccentColor}" /> <SolidColorBrush x:Key="ThemeAccentColorBrush" Color="{DynamicResource SystemAccentColor}" />
<StaticResource x:Key="ListViewItemBackgroundSelected" ResourceKey="ThemeAccentColorBrush" /> <StaticResource x:Key="ListViewItemBackgroundSelected" ResourceKey="ThemeAccentColorBrush" />
@@ -271,6 +306,9 @@
<Color x:Key="ThemeControlBorderColor">#FF505050</Color> <Color x:Key="ThemeControlBorderColor">#FF505050</Color>
<Color x:Key="VsyncEnabled">#FF2EEAC9</Color> <Color x:Key="VsyncEnabled">#FF2EEAC9</Color>
<Color x:Key="VsyncDisabled">#FFFF4554</Color> <Color x:Key="VsyncDisabled">#FFFF4554</Color>
<Color x:Key="AppListBackgroundColor">#0FFFFFFF</Color>
<Color x:Key="AppListHoverBackgroundColor">#1EFFFFFF</Color>
<Color x:Key="SecondaryTextColor">#A0FFFFFF</Color>
<x:Double x:Key="ScrollBarThickness">15</x:Double> <x:Double x:Key="ScrollBarThickness">15</x:Double>
<x:Double x:Key="FontSizeSmall">8</x:Double> <x:Double x:Key="FontSizeSmall">8</x:Double>
<x:Double x:Key="FontSizeNormal">10</x:Double> <x:Double x:Key="FontSizeNormal">10</x:Double>

View File

@@ -1,6 +1,7 @@
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using FluentAvalonia.Core;
using Ryujinx.Input; using Ryujinx.Input;
using System; using System;
using System.Numerics; using System.Numerics;
@@ -69,12 +70,22 @@ namespace Ryujinx.Ava.Input
private void Parent_PointerReleaseEvent(object o, PointerReleasedEventArgs args) private void Parent_PointerReleaseEvent(object o, PointerReleasedEventArgs args)
{ {
PressedButtons[(int)args.InitialPressMouseButton - 1] = false; int button = (int)args.InitialPressMouseButton - 1;
if (PressedButtons.Count() >= button)
{
PressedButtons[button] = false;
}
} }
private void Parent_PointerPressEvent(object o, PointerPressedEventArgs args) private void Parent_PointerPressEvent(object o, PointerPressedEventArgs args)
{ {
PressedButtons[(int)args.GetCurrentPoint(_widget).Properties.PointerUpdateKind] = true; int button = (int)args.GetCurrentPoint(_widget).Properties.PointerUpdateKind;
if (PressedButtons.Count() >= button)
{
PressedButtons[button] = true;
}
} }
private void Parent_PointerMovedEvent(object o, PointerEventArgs args) private void Parent_PointerMovedEvent(object o, PointerEventArgs args)
@@ -85,14 +96,20 @@ namespace Ryujinx.Ava.Input
} }
public void SetMousePressed(MouseButton button) public void SetMousePressed(MouseButton button)
{
if (PressedButtons.Count() >= (int)button)
{ {
PressedButtons[(int)button] = true; PressedButtons[(int)button] = true;
} }
}
public void SetMouseReleased(MouseButton button) public void SetMouseReleased(MouseButton button)
{
if (PressedButtons.Count() >= (int)button)
{ {
PressedButtons[(int)button] = false; PressedButtons[(int)button] = false;
} }
}
public void SetPosition(double x, double y) public void SetPosition(double x, double y)
{ {
@@ -100,10 +117,15 @@ namespace Ryujinx.Ava.Input
} }
public bool IsButtonPressed(MouseButton button) public bool IsButtonPressed(MouseButton button)
{
if (PressedButtons.Count() >= (int)button)
{ {
return PressedButtons[(int)button]; return PressedButtons[(int)button];
} }
return false;
}
public Size GetClientSize() public Size GetClientSize()
{ {
return _size; return _size;

View File

@@ -1,6 +1,6 @@
using Avalonia; using Avalonia;
using Avalonia.Threading; using Avalonia.Threading;
using Ryujinx.Ava.UI.Helper; using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;

View File

@@ -130,6 +130,18 @@
<DependentUpon>GameListView.axaml</DependentUpon> <DependentUpon>GameListView.axaml</DependentUpon>
<SubType>Code</SubType> <SubType>Code</SubType>
</Compile> </Compile>
<Compile Update="UI\Views\User\UserEditorView.axaml.cs">
<DependentUpon>UserEditor.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="UI\Views\User\UserRecovererView.axaml.cs">
<DependentUpon>UserRecoverer.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="UI\Views\User\UserSelectorView.axaml.cs">
<DependentUpon>UserSelector.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

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

View File

@@ -112,32 +112,8 @@
</ListBox.ItemsPanel> </ListBox.ItemsPanel>
<ListBox.Styles> <ListBox.Styles>
<Style Selector="ListBoxItem"> <Style Selector="ListBoxItem">
<Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="5" /> <Setter Property="Margin" Value="5" />
<Setter Property="CornerRadius" Value="4" /> <Setter Property="CornerRadius" Value="4" />
<Setter Property="Background" Value="{DynamicResource AppListBackgroundColor}" />
<Style.Animations>
<Animation Duration="0:0:0.7">
<KeyFrame Cue="0%">
<Setter Property="MaxWidth" Value="0" />
<Setter Property="Opacity" Value="0.0" />
</KeyFrame>
<KeyFrame Cue="50%">
<Setter Property="MaxWidth" Value="1000" />
<Setter Property="Opacity" Value="0.3" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="MaxWidth" Value="1000" />
<Setter Property="Opacity" Value="1.0" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="ListBoxItem:selected /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AppListBackgroundColor}" />
</Style>
<Style Selector="ListBoxItem:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AppListHoverBackgroundColor}" />
</Style> </Style>
<Style Selector="ListBoxItem:selected /template/ Border#SelectionIndicator"> <Style Selector="ListBoxItem:selected /template/ Border#SelectionIndicator">
<Setter Property="MinHeight" Value="{Binding $parent[UserControl].DataContext.GridItemSelectorSize}" /> <Setter Property="MinHeight" Value="{Binding $parent[UserControl].DataContext.GridItemSelectorSize}" />

View File

@@ -3,14 +3,14 @@
xmlns="https://github.com/avaloniaui" xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
d:DesignHeight="450" d:DesignHeight="450"
d:DesignWidth="800" d:DesignWidth="800"
mc:Ignorable="d" Focusable="True"
Focusable="True"> mc:Ignorable="d">
<UserControl.Resources> <UserControl.Resources>
<helpers:BitmapArrayValueConverter x:Key="ByteImage" /> <helpers:BitmapArrayValueConverter x:Key="ByteImage" />
<MenuFlyout x:Key="GameContextMenu" Opened="MenuBase_OnMenuOpened"> <MenuFlyout x:Key="GameContextMenu" Opened="MenuBase_OnMenuOpened">
@@ -111,35 +111,6 @@
</ItemsPanelTemplate> </ItemsPanelTemplate>
</ListBox.ItemsPanel> </ListBox.ItemsPanel>
<ListBox.Styles> <ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="0" />
<Setter Property="CornerRadius" Value="5" />
<Setter Property="Background" Value="{DynamicResource AppListBackgroundColor}" />
<Setter Property="BorderThickness" Value="2"/>
<Style.Animations>
<Animation Duration="0:0:0.7">
<KeyFrame Cue="0%">
<Setter Property="MaxHeight" Value="0" />
<Setter Property="Opacity" Value="0.0" />
</KeyFrame>
<KeyFrame Cue="50%">
<Setter Property="MaxHeight" Value="1000" />
<Setter Property="Opacity" Value="0.3" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="MaxHeight" Value="1000" />
<Setter Property="Opacity" Value="1.0" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="ListBoxItem:selected /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AppListBackgroundColor}" />
</Style>
<Style Selector="ListBoxItem:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AppListHoverBackgroundColor}" />
</Style>
<Style Selector="ListBoxItem:selected /template/ Border#SelectionIndicator"> <Style Selector="ListBoxItem:selected /template/ Border#SelectionIndicator">
<Setter Property="MinHeight" Value="{Binding $parent[UserControl].DataContext.ListItemSelectorSize}" /> <Setter Property="MinHeight" Value="{Binding $parent[UserControl].DataContext.ListItemSelectorSize}" />
</Style> </Style>
@@ -159,7 +130,8 @@
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
<ColumnDefinition Width="10" /> <ColumnDefinition Width="10" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="150" />
<ColumnDefinition Width="100" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Image <Image
Grid.RowSpan="3" Grid.RowSpan="3"
@@ -170,14 +142,19 @@
Classes.normal="{Binding $parent[UserControl].DataContext.IsGridMedium}" Classes.normal="{Binding $parent[UserControl].DataContext.IsGridMedium}"
Classes.small="{Binding $parent[UserControl].DataContext.IsGridSmall}" Classes.small="{Binding $parent[UserControl].DataContext.IsGridSmall}"
Source="{Binding Icon, Converter={StaticResource ByteImage}}" /> Source="{Binding Icon, Converter={StaticResource ByteImage}}" />
<StackPanel <Border
Grid.Column="2" Grid.Column="2"
Margin="0,0,5,0"
BorderBrush="{DynamicResource ThemeControlBorderColor}"
BorderThickness="0,0,1,0">
<StackPanel
HorizontalAlignment="Left" HorizontalAlignment="Left"
VerticalAlignment="Top" VerticalAlignment="Top"
Orientation="Vertical" Orientation="Vertical"
Spacing="5" > Spacing="5">
<TextBlock <TextBlock
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
FontWeight="Bold"
Text="{Binding TitleName}" Text="{Binding TitleName}"
TextAlignment="Left" TextAlignment="Left"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
@@ -192,8 +169,27 @@
TextAlignment="Left" TextAlignment="Left"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
</StackPanel> </StackPanel>
</Border>
<StackPanel <StackPanel
Grid.Column="3" Grid.Column="3"
Margin="10,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Orientation="Vertical"
Spacing="5">
<TextBlock
HorizontalAlignment="Stretch"
Text="{Binding TitleId}"
TextAlignment="Left"
TextWrapping="Wrap" />
<TextBlock
HorizontalAlignment="Stretch"
Text="{Binding FileExtension}"
TextAlignment="Left"
TextWrapping="Wrap" />
</StackPanel>
<StackPanel
Grid.Column="4"
HorizontalAlignment="Right" HorizontalAlignment="Right"
VerticalAlignment="Top" VerticalAlignment="Top"
Orientation="Vertical" Orientation="Vertical"

View File

@@ -12,5 +12,6 @@
<ui:Frame <ui:Frame
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
x:Name="ContentFrame" /> x:Name="ContentFrame">
</ui:Frame>
</UserControl> </UserControl>

View File

@@ -1,13 +1,25 @@
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Styling;
using Avalonia.Threading;
using FluentAvalonia.Core;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using LibHac; using LibHac;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Shim;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Views.User;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.HLE.HOS.Services.Account.Acc;
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;
using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
namespace Ryujinx.Ava.UI.Controls namespace Ryujinx.Ava.UI.Controls
{ {
@@ -31,14 +43,14 @@ namespace Ryujinx.Ava.UI.Controls
ContentManager = contentManager; ContentManager = contentManager;
VirtualFileSystem = virtualFileSystem; VirtualFileSystem = virtualFileSystem;
HorizonClient = horizonClient; HorizonClient = horizonClient;
ViewModel = new UserProfileViewModel(this); ViewModel = new UserProfileViewModel();
LoadProfiles();
if (contentManager.GetCurrentFirmwareVersion() != null) if (contentManager.GetCurrentFirmwareVersion() != null)
{ {
Task.Run(() => Task.Run(() =>
{ {
AvatarProfileViewModel.PreloadAvatars(contentManager, virtualFileSystem); UserFirmwareAvatarSelectorViewModel.PreloadAvatars(contentManager, virtualFileSystem);
}); });
} }
InitializeComponent(); InitializeComponent();
@@ -51,7 +63,7 @@ namespace Ryujinx.Ava.UI.Controls
ContentFrame.GoBack(); ContentFrame.GoBack();
} }
ViewModel.LoadProfiles(); LoadProfiles();
} }
public void Navigate(Type sourcePageType, object parameter) public void Navigate(Type sourcePageType, object parameter)
@@ -68,7 +80,7 @@ namespace Ryujinx.Ava.UI.Controls
Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle], Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle],
PrimaryButtonText = "", PrimaryButtonText = "",
SecondaryButtonText = "", SecondaryButtonText = "",
CloseButtonText = LocaleManager.Instance[LocaleKeys.UserProfilesClose], CloseButtonText = "",
Content = content, Content = content,
Padding = new Thickness(0) Padding = new Thickness(0)
}; };
@@ -78,6 +90,11 @@ namespace Ryujinx.Ava.UI.Controls
content.ViewModel.Dispose(); content.ViewModel.Dispose();
}; };
Style footer = new(x => x.Name("DialogSpace").Child().OfType<Border>());
footer.Setters.Add(new Setter(IsVisibleProperty, false));
contentDialog.Styles.Add(footer);
await contentDialog.ShowAsync(); await contentDialog.ShowAsync();
} }
@@ -85,7 +102,117 @@ namespace Ryujinx.Ava.UI.Controls
{ {
base.OnAttachedToVisualTree(e); base.OnAttachedToVisualTree(e);
Navigate(typeof(UserSelector), this); Navigate(typeof(UserSelectorViews), this);
}
public void LoadProfiles()
{
ViewModel.Profiles.Clear();
ViewModel.LostProfiles.Clear();
var profiles = AccountManager.GetAllUsers().OrderBy(x => x.Name);
foreach (var profile in profiles)
{
ViewModel.Profiles.Add(new UserProfile(profile, this));
}
var saveDataFilter = SaveDataFilter.Make(programId: default, saveType: SaveDataType.Account, default, saveDataId: default, index: default);
using var saveDataIterator = new UniqueRef<SaveDataIterator>();
HorizonClient.Fs.OpenSaveDataIterator(ref saveDataIterator.Ref(), SaveDataSpaceId.User, in saveDataFilter).ThrowIfFailure();
Span<SaveDataInfo> saveDataInfo = stackalloc SaveDataInfo[10];
HashSet<HLE.HOS.Services.Account.Acc.UserId> lostAccounts = new();
while (true)
{
saveDataIterator.Get.ReadSaveDataInfo(out long readCount, saveDataInfo).ThrowIfFailure();
if (readCount == 0)
{
break;
}
for (int i = 0; i < readCount; i++)
{
var save = saveDataInfo[i];
var id = new HLE.HOS.Services.Account.Acc.UserId((long)save.UserId.Id.Low, (long)save.UserId.Id.High);
if (ViewModel.Profiles.Cast<UserProfile>().FirstOrDefault( x=> x.UserId == id) == null)
{
lostAccounts.Add(id);
}
}
}
foreach(var account in lostAccounts)
{
ViewModel.LostProfiles.Add(new UserProfile(new HLE.HOS.Services.Account.Acc.UserProfile(account, "", null), this));
}
ViewModel.Profiles.Add(new BaseModel());
}
public async void DeleteUser(UserProfile userProfile)
{
var lastUserId = AccountManager.LastOpenedUser.UserId;
if (userProfile.UserId == lastUserId)
{
// If we are deleting the currently open profile, then we must open something else before deleting.
var profile = ViewModel.Profiles.Cast<UserProfile>().FirstOrDefault(x => x.UserId != lastUserId);
if (profile == null)
{
async void Action()
{
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUserProfileDeletionWarningMessage]);
}
Dispatcher.UIThread.Post(Action);
return;
}
AccountManager.OpenUser(profile.UserId);
}
var result = await ContentDialogHelper.CreateConfirmationDialog(
LocaleManager.Instance[LocaleKeys.DialogUserProfileDeletionConfirmMessage],
"",
LocaleManager.Instance[LocaleKeys.InputDialogYes],
LocaleManager.Instance[LocaleKeys.InputDialogNo],
"");
if (result == UserResult.Yes)
{
GoBack();
AccountManager.DeleteUser(userProfile.UserId);
}
LoadProfiles();
}
public void AddUser()
{
Navigate(typeof(UserEditorView), (this, (UserProfile)null, true));
}
public void EditUser(UserProfile userProfile)
{
Navigate(typeof(UserEditorView), (this, userProfile, false));
}
public void RecoverLostAccounts()
{
Navigate(typeof(UserRecovererView), this);
}
public void ManageSaves()
{
Navigate(typeof(UserSaveManagerView), (this, AccountManager, HorizonClient, VirtualFileSystem));
} }
} }
} }

View File

@@ -1,57 +0,0 @@
<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"
x:Class="Ryujinx.Ava.UI.Controls.ProfileImageSelectionDialog"
Focusable="True">
<Grid
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Margin="5,10,5, 5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="70" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock
FontWeight="Bold"
FontSize="18"
HorizontalAlignment="Center"
Grid.Row="1"
Text="{locale:Locale ProfileImageSelectionHeader}" />
<TextBlock
FontWeight="Bold"
Grid.Row="2"
Margin="10"
MaxWidth="400"
TextWrapping="Wrap"
HorizontalAlignment="Center"
TextAlignment="Center"
Text="{locale:Locale ProfileImageSelectionNote}" />
<StackPanel
Margin="5,0"
Spacing="10"
Grid.Row="4"
HorizontalAlignment="Center"
Orientation="Horizontal">
<Button
Name="Import"
Click="Import_OnClick"
Width="200">
<TextBlock Text="{locale:Locale ProfileImageSelectionImportImage}" />
</Button>
<Button
Name="SelectFirmwareImage"
IsEnabled="{Binding FirmwareFound}"
Click="SelectFirmwareImage_OnClick"
Width="200">
<TextBlock Text="{locale:Locale ProfileImageSelectionSelectAvatar}" />
</Button>
</StackPanel>
</Grid>
</UserControl>

View File

@@ -1,127 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Common.Configuration;
using Silk.NET.Vulkan;
using SPB.Graphics.OpenGL;
using SPB.Windowing;
using System;
namespace Ryujinx.Ava.UI.Controls
{
public partial class RendererHost : UserControl, IDisposable
{
private readonly GraphicsDebugLevel _graphicsDebugLevel;
private EmbeddedWindow _currentWindow;
public bool IsVulkan { get; private set; }
public RendererHost(GraphicsDebugLevel graphicsDebugLevel)
{
_graphicsDebugLevel = graphicsDebugLevel;
InitializeComponent();
}
public RendererHost()
{
InitializeComponent();
}
public void CreateOpenGL()
{
Dispose();
_currentWindow = new OpenGLEmbeddedWindow(3, 3, _graphicsDebugLevel);
Initialize();
IsVulkan = false;
}
private void Initialize()
{
_currentWindow.WindowCreated += CurrentWindow_WindowCreated;
_currentWindow.SizeChanged += CurrentWindow_SizeChanged;
Content = _currentWindow;
}
public void CreateVulkan()
{
Dispose();
_currentWindow = new VulkanEmbeddedWindow();
Initialize();
IsVulkan = true;
}
public OpenGLContextBase GetContext()
{
if (_currentWindow is OpenGLEmbeddedWindow openGlEmbeddedWindow)
{
return openGlEmbeddedWindow.Context;
}
return null;
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
Dispose();
}
private void CurrentWindow_SizeChanged(object sender, Size e)
{
SizeChanged?.Invoke(sender, e);
}
private void CurrentWindow_WindowCreated(object sender, IntPtr e)
{
RendererInitialized?.Invoke(this, EventArgs.Empty);
}
public void MakeCurrent()
{
if (_currentWindow is OpenGLEmbeddedWindow openGlEmbeddedWindow)
{
openGlEmbeddedWindow.MakeCurrent();
}
}
public void MakeCurrent(SwappableNativeWindowBase window)
{
if (_currentWindow is OpenGLEmbeddedWindow openGlEmbeddedWindow)
{
openGlEmbeddedWindow.MakeCurrent(window);
}
}
public void SwapBuffers()
{
if (_currentWindow is OpenGLEmbeddedWindow openGlEmbeddedWindow)
{
openGlEmbeddedWindow.SwapBuffers();
}
}
public event EventHandler<EventArgs> RendererInitialized;
public event Action<object, Size> SizeChanged;
public void Dispose()
{
if (_currentWindow != null)
{
_currentWindow.WindowCreated -= CurrentWindow_WindowCreated;
_currentWindow.SizeChanged -= CurrentWindow_SizeChanged;
}
}
public SurfaceKHR CreateVulkanSurface(Instance instance, Vk api)
{
return (_currentWindow is VulkanEmbeddedWindow vulkanEmbeddedWindow)
? vulkanEmbeddedWindow.CreateSurface(instance)
: default;
}
}
}

View File

@@ -1,175 +0,0 @@
<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:models="clr-namespace:Ryujinx.Ava.UI.Models"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
mc:Ignorable="d"
d:DesignWidth="800"
d:DesignHeight="450"
Height="400"
Width="550"
x:Class="Ryujinx.Ava.UI.Controls.SaveManager"
Focusable="True">
<UserControl.Resources>
<helpers:BitmapArrayValueConverter x:Key="ByteImage" />
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Grid
Grid.Row="0"
HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<StackPanel
Spacing="10"
Orientation="Horizontal"
HorizontalAlignment="Left"
VerticalAlignment="Center">
<Label
Content="{locale:Locale CommonSort}"
VerticalAlignment="Center" />
<ComboBox SelectedIndex="{Binding SortIndex}" Width="100">
<ComboBoxItem>
<Label
VerticalAlignment="Center"
HorizontalContentAlignment="Left"
Content="{locale:Locale Name}" />
</ComboBoxItem>
<ComboBoxItem>
<Label
VerticalAlignment="Center"
HorizontalContentAlignment="Left"
Content="{locale:Locale Size}" />
</ComboBoxItem>
</ComboBox>
<ComboBox SelectedIndex="{Binding OrderIndex}" Width="150">
<ComboBoxItem>
<Label
VerticalAlignment="Center"
HorizontalContentAlignment="Left"
Content="{locale:Locale OrderAscending}" />
</ComboBoxItem>
<ComboBoxItem>
<Label
VerticalAlignment="Center"
HorizontalContentAlignment="Left"
Content="{locale:Locale OrderDescending}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
<Grid
Grid.Column="1"
HorizontalAlignment="Stretch"
Margin="10,0, 0, 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Label
Content="{locale:Locale Search}"
VerticalAlignment="Center"/>
<TextBox
Margin="5,0,0,0"
Grid.Column="1"
HorizontalAlignment="Stretch"
Text="{Binding Search}"/>
</Grid>
</Grid>
<Border
Grid.Row="1"
Margin="0,5"
BorderThickness="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<ListBox
Name="SaveList"
Items="{Binding View}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="models:SaveModel">
<Grid HorizontalAlignment="Stretch" Margin="0,5">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal">
<Border
Height="42"
Margin="2"
Width="42"
Padding="10"
IsVisible="{Binding !InGameList}">
<ui:SymbolIcon
Symbol="Help"
FontSize="30"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<Image
IsVisible="{Binding InGameList}"
Margin="2"
Width="42"
Height="42"
Source="{Binding Icon,
Converter={StaticResource ByteImage}}" />
<TextBlock
MaxLines="3"
Width="320"
Margin="5"
TextWrapping="Wrap"
Text="{Binding Title}" VerticalAlignment="Center" />
</StackPanel>
<StackPanel
Grid.Column="1"
Spacing="10"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Label
Content="{Binding SizeString}"
IsVisible="{Binding SizeAvailable}"
VerticalAlignment="Center"
HorizontalAlignment="Right" />
<Button
VerticalAlignment="Center"
HorizontalAlignment="Right"
Padding="10"
MinWidth="0"
MinHeight="0"
Name="OpenLocation"
Command="{Binding OpenLocation}">
<ui:SymbolIcon
Symbol="OpenFolder"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Button>
<Button
VerticalAlignment="Center"
HorizontalAlignment="Right"
Padding="10"
MinWidth="0"
MinHeight="0"
Name="Delete"
Command="{Binding Delete}">
<ui:SymbolIcon
Symbol="Delete"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Button>
</StackPanel>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</Grid>
</UserControl>

View File

@@ -1,160 +0,0 @@
using Avalonia.Controls;
using DynamicData;
using DynamicData.Binding;
using LibHac;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Shim;
using Ryujinx.Ava.Common;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Models;
using Ryujinx.HLE.FileSystem;
using Ryujinx.Ui.App.Common;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
namespace Ryujinx.Ava.UI.Controls
{
public partial class SaveManager : UserControl
{
private readonly UserProfile _userProfile;
private readonly HorizonClient _horizonClient;
private readonly VirtualFileSystem _virtualFileSystem;
private int _sortIndex;
private int _orderIndex;
private ObservableCollection<SaveModel> _view = new ObservableCollection<SaveModel>();
private string _search;
public ObservableCollection<SaveModel> Saves { get; set; } = new ObservableCollection<SaveModel>();
public ObservableCollection<SaveModel> View
{
get => _view;
set => _view = value;
}
public int SortIndex
{
get => _sortIndex;
set
{
_sortIndex = value;
Sort();
}
}
public int OrderIndex
{
get => _orderIndex;
set
{
_orderIndex = value;
Sort();
}
}
public string Search
{
get => _search;
set
{
_search = value;
Sort();
}
}
public SaveManager()
{
InitializeComponent();
}
public SaveManager(UserProfile userProfile, HorizonClient horizonClient, VirtualFileSystem virtualFileSystem)
{
_userProfile = userProfile;
_horizonClient = horizonClient;
_virtualFileSystem = virtualFileSystem;
InitializeComponent();
DataContext = this;
Task.Run(LoadSaves);
}
public void LoadSaves()
{
Saves.Clear();
var saveDataFilter = SaveDataFilter.Make(programId: default, saveType: SaveDataType.Account,
new UserId((ulong)_userProfile.UserId.High, (ulong)_userProfile.UserId.Low), saveDataId: default, index: default);
using var saveDataIterator = new UniqueRef<SaveDataIterator>();
_horizonClient.Fs.OpenSaveDataIterator(ref saveDataIterator.Ref(), SaveDataSpaceId.User, in saveDataFilter).ThrowIfFailure();
Span<SaveDataInfo> saveDataInfo = stackalloc SaveDataInfo[10];
while (true)
{
saveDataIterator.Get.ReadSaveDataInfo(out long readCount, saveDataInfo).ThrowIfFailure();
if (readCount == 0)
{
break;
}
for (int i = 0; i < readCount; i++)
{
var save = saveDataInfo[i];
if (save.ProgramId.Value != 0)
{
var saveModel = new SaveModel(save, _horizonClient, _virtualFileSystem);
Saves.Add(saveModel);
saveModel.DeleteAction = () => { Saves.Remove(saveModel); };
}
Sort();
}
}
}
private void Sort()
{
Saves.AsObservableChangeSet()
.Filter(Filter)
.Sort(GetComparer())
.Bind(out var view).AsObservableList();
_view.Clear();
_view.AddRange(view);
}
private IComparer<SaveModel> GetComparer()
{
switch (SortIndex)
{
case 0:
return OrderIndex == 0
? SortExpressionComparer<SaveModel>.Ascending(save => save.Title)
: SortExpressionComparer<SaveModel>.Descending(save => save.Title);
case 1:
return OrderIndex == 0
? SortExpressionComparer<SaveModel>.Ascending(save => save.Size)
: SortExpressionComparer<SaveModel>.Descending(save => save.Size);
default:
return null;
}
}
private bool Filter(object arg)
{
if (arg is SaveModel save)
{
return string.IsNullOrWhiteSpace(_search) || save.Title.ToLower().Contains(_search.ToLower());
}
return false;
}
}
}

View File

@@ -1,72 +0,0 @@
<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"
MinWidth="500"
MinHeight="400"
x:Class="Ryujinx.Ava.UI.Controls.UserRecoverer"
Focusable="True">
<Design.DataContext>
<viewModels:UserProfileViewModel />
</Design.DataContext>
<Grid HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Button Grid.Row="0"
Margin="5"
Height="30"
Width="50"
MinWidth="50"
HorizontalAlignment="Left"
Command="{Binding GoBack}">
<ui:SymbolIcon Symbol="Back"/>
</Button>
<TextBlock Grid.Row="1"
Text="{locale:Locale UserProfilesRecoverHeading}"/>
<ListBox
Margin="5"
Grid.Row="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Items="{Binding LostProfiles}">
<ListBox.ItemTemplate>
<DataTemplate>
<Border
Margin="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ClipToBounds="True"
CornerRadius="5">
<Grid Margin="0">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock
HorizontalAlignment="Stretch"
Text="{Binding UserId}"
TextAlignment="Left"
TextWrapping="Wrap" />
<Button Grid.Column="1"
HorizontalAlignment="Right"
Command="{Binding Recover}"
CommandParameter="{Binding}"
Content="{locale:Locale Recover}"/>
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</UserControl>

View File

@@ -1,44 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Navigation;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.ViewModels;
namespace Ryujinx.Ava.UI.Controls
{
public partial class UserRecoverer : UserControl
{
private UserProfileViewModel _viewModel;
private NavigationDialogHost _parent;
public UserRecoverer()
{
InitializeComponent();
AddHandler(Frame.NavigatedToEvent, (s, e) =>
{
NavigatedTo(e);
}, RoutingStrategies.Direct);
}
private void NavigatedTo(NavigationEventArgs arg)
{
if (Program.PreviewerDetached)
{
switch (arg.NavigationMode)
{
case NavigationMode.New:
var args = ((NavigationDialogHost parent, UserProfileViewModel viewModel))arg.Parameter;
_viewModel = args.viewModel;
_parent = args.parent;
break;
}
DataContext = _viewModel;
}
}
}
}

View File

@@ -1,145 +0,0 @@
<UserControl
x:Class="Ryujinx.Ava.UI.Controls.UserSelector"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:flex="clr-namespace:Avalonia.Flexbox;assembly=Avalonia.Flexbox"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
d:DesignHeight="450"
MinWidth="500"
d:DesignWidth="800"
mc:Ignorable="d"
Focusable="True">
<UserControl.Resources>
<helpers:BitmapArrayValueConverter x:Key="ByteImage" />
</UserControl.Resources>
<Design.DataContext>
<viewModels:UserProfileViewModel />
</Design.DataContext>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox
Margin="5"
MaxHeight="300"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
DoubleTapped="ProfilesList_DoubleTapped"
Items="{Binding Profiles}"
SelectionChanged="SelectingItemsControl_SelectionChanged">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<flex:FlexPanel
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
AlignContent="FlexStart"
JustifyContent="Center" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Border
Margin="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ClipToBounds="True"
CornerRadius="5">
<Grid Margin="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Image
Grid.Row="0"
Width="96"
Height="96"
Margin="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Source="{Binding Image, Converter={StaticResource ByteImage}}" />
<StackPanel
Grid.Row="1"
Height="30"
Margin="5"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<TextBlock
HorizontalAlignment="Stretch"
Text="{Binding Name}"
TextAlignment="Center"
TextWrapping="Wrap" />
</StackPanel>
</Grid>
</Border>
<Border
Width="10"
Height="10"
Margin="5"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Background="LimeGreen"
CornerRadius="5"
IsVisible="{Binding IsOpened}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Grid
Grid.Row="1"
HorizontalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Button
HorizontalAlignment="Stretch"
Grid.Row="0"
Grid.Column="0"
Margin="2"
Command="{Binding AddUser}"
Content="{locale:Locale UserProfilesAddNewProfile}" />
<Button
HorizontalAlignment="Stretch"
Grid.Row="0"
Margin="2"
Grid.Column="1"
Command="{Binding EditUser}"
Content="{locale:Locale UserProfilesEditProfile}"
IsEnabled="{Binding IsSelectedProfiledEditable}" />
<Button
HorizontalAlignment="Stretch"
Grid.Row="1"
Grid.Column="0"
Margin="2"
Content="{locale:Locale UserProfilesManageSaves}"
Command="{Binding ManageSaves}" />
<Button
HorizontalAlignment="Stretch"
Grid.Row="1"
Grid.Column="1"
Margin="2"
Command="{Binding DeleteUser}"
Content="{locale:Locale UserProfilesDeleteSelectedProfile}"
IsEnabled="{Binding IsSelectedProfileDeletable}" />
<Button
HorizontalAlignment="Stretch"
Grid.Row="2"
Grid.ColumnSpan="2"
Grid.Column="0"
Margin="2"
Command="{Binding RecoverLostAccounts}"
Content="{locale:Locale UserProfilesRecoverLostAccounts}" />
</Grid>
</Grid>
</UserControl>

View File

@@ -1,77 +0,0 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Navigation;
using Ryujinx.Ava.UI.ViewModels;
using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
namespace Ryujinx.Ava.UI.Controls
{
public partial class UserSelector : UserControl
{
private NavigationDialogHost _parent;
public UserProfileViewModel ViewModel { get; set; }
public UserSelector()
{
InitializeComponent();
if (Program.PreviewerDetached)
{
AddHandler(Frame.NavigatedToEvent, (s, e) =>
{
NavigatedTo(e);
}, RoutingStrategies.Direct);
}
}
private void NavigatedTo(NavigationEventArgs arg)
{
if (Program.PreviewerDetached)
{
if (arg.NavigationMode == NavigationMode.New)
{
_parent = (NavigationDialogHost)arg.Parameter;
ViewModel = _parent.ViewModel;
}
DataContext = ViewModel;
}
}
private void ProfilesList_DoubleTapped(object sender, RoutedEventArgs e)
{
if (sender is ListBox listBox)
{
int selectedIndex = listBox.SelectedIndex;
if (selectedIndex >= 0 && selectedIndex < ViewModel.Profiles.Count)
{
ViewModel.SelectedProfile = ViewModel.Profiles[selectedIndex];
_parent?.AccountManager?.OpenUser(ViewModel.SelectedProfile.UserId);
ViewModel.LoadProfiles();
foreach (UserProfile profile in ViewModel.Profiles)
{
profile.UpdateState();
}
}
}
}
private void SelectingItemsControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is ListBox listBox)
{
int selectedIndex = listBox.SelectedIndex;
if (selectedIndex >= 0 && selectedIndex < ViewModel.Profiles.Count)
{
ViewModel.HighlightedProfile = ViewModel.Profiles[selectedIndex];
}
}
}
}
}

View File

@@ -1,16 +0,0 @@
using SPB.Graphics;
using System;
using System.Runtime.Versioning;
namespace Ryujinx.Ava.UI.Helpers
{
[SupportedOSPlatform("linux")]
internal class AvaloniaGlxContext : SPB.Platform.GLX.GLXOpenGLContext
{
public AvaloniaGlxContext(IntPtr handle)
: base(FramebufferFormat.Default, 0, 0, 0, false, null)
{
ContextHandle = handle;
}
}
}

View File

@@ -1,16 +0,0 @@
using SPB.Graphics;
using System;
using System.Runtime.Versioning;
namespace Ryujinx.Ava.UI.Helpers
{
[SupportedOSPlatform("windows")]
internal class AvaloniaWglContext : SPB.Platform.WGL.WGLOpenGLContext
{
public AvaloniaWglContext(IntPtr handle)
: base(FramebufferFormat.Default, 0, 0, 0, false, null)
{
ContextHandle = handle;
}
}
}

View File

@@ -1,233 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Platform;
using Ryujinx.Ava.UI.Helper;
using SPB.Graphics;
using SPB.Platform;
using SPB.Platform.GLX;
using System;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop;
namespace Ryujinx.Ava.UI.Helpers
{
public class EmbeddedWindow : NativeControlHost
{
private WindowProc _wndProcDelegate;
private string _className;
protected GLXWindow X11Window { get; set; }
protected IntPtr WindowHandle { get; set; }
protected IntPtr X11Display { get; set; }
protected IntPtr NsView { get; set; }
protected IntPtr MetalLayer { get; set; }
private UpdateBoundsCallbackDelegate _updateBoundsCallback;
public event EventHandler<IntPtr> WindowCreated;
public event EventHandler<Size> SizeChanged;
protected virtual void OnWindowDestroyed() { }
protected virtual void OnWindowDestroying()
{
WindowHandle = IntPtr.Zero;
X11Display = IntPtr.Zero;
}
public EmbeddedWindow()
{
var stateObserverable = this.GetObservable(BoundsProperty);
stateObserverable.Subscribe(StateChanged);
this.Initialized += NativeEmbeddedWindow_Initialized;
}
public virtual void OnWindowCreated() { }
private void NativeEmbeddedWindow_Initialized(object sender, EventArgs e)
{
OnWindowCreated();
Task.Run(() =>
{
WindowCreated?.Invoke(this, WindowHandle);
});
}
private void StateChanged(Rect rect)
{
SizeChanged?.Invoke(this, rect.Size);
_updateBoundsCallback?.Invoke(rect);
}
protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent)
{
if (OperatingSystem.IsLinux())
{
return CreateLinux(parent);
}
else if (OperatingSystem.IsWindows())
{
return CreateWin32(parent);
}
else if (OperatingSystem.IsMacOS())
{
return CreateMacOs(parent);
}
return base.CreateNativeControlCore(parent);
}
protected override void DestroyNativeControlCore(IPlatformHandle control)
{
OnWindowDestroying();
if (OperatingSystem.IsLinux())
{
DestroyLinux();
}
else if (OperatingSystem.IsWindows())
{
DestroyWin32(control);
}
else if (OperatingSystem.IsMacOS())
{
DestroyMacOS();
}
else
{
base.DestroyNativeControlCore(control);
}
OnWindowDestroyed();
}
[SupportedOSPlatform("linux")]
protected virtual IPlatformHandle CreateLinux(IPlatformHandle parent)
{
X11Window = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100) as GLXWindow;
WindowHandle = X11Window.WindowHandle.RawHandle;
X11Display = X11Window.DisplayHandle.RawHandle;
return new PlatformHandle(WindowHandle, "X11");
}
[SupportedOSPlatform("windows")]
IPlatformHandle CreateWin32(IPlatformHandle parent)
{
_className = "NativeWindow-" + Guid.NewGuid();
_wndProcDelegate = WndProc;
var wndClassEx = new WNDCLASSEX
{
cbSize = Marshal.SizeOf<WNDCLASSEX>(),
hInstance = GetModuleHandle(null),
lpfnWndProc = Marshal.GetFunctionPointerForDelegate(_wndProcDelegate),
style = ClassStyles.CS_OWNDC,
lpszClassName = Marshal.StringToHGlobalUni(_className),
hCursor = LoadCursor(IntPtr.Zero, (IntPtr)Cursors.IDC_ARROW)
};
var atom = RegisterClassEx(ref wndClassEx);
var handle = CreateWindowEx(
0,
_className,
"NativeWindow",
WindowStyles.WS_CHILD,
0,
0,
640,
480,
parent.Handle,
IntPtr.Zero,
IntPtr.Zero,
IntPtr.Zero);
WindowHandle = handle;
Marshal.FreeHGlobal(wndClassEx.lpszClassName);
return new PlatformHandle(WindowHandle, "HWND");
}
[SupportedOSPlatform("windows")]
IntPtr WndProc(IntPtr hWnd, WindowsMessages msg, IntPtr wParam, IntPtr lParam)
{
var point = new Point((long)lParam & 0xFFFF, ((long)lParam >> 16) & 0xFFFF);
var root = VisualRoot as Window;
bool isLeft = false;
switch (msg)
{
case WindowsMessages.LBUTTONDOWN:
case WindowsMessages.RBUTTONDOWN:
isLeft = msg == WindowsMessages.LBUTTONDOWN;
this.RaiseEvent(new PointerPressedEventArgs(
this,
new Pointer(0, PointerType.Mouse, true),
root,
this.TranslatePoint(point, root).Value,
(ulong)Environment.TickCount64,
new PointerPointProperties(isLeft ? RawInputModifiers.LeftMouseButton : RawInputModifiers.RightMouseButton, isLeft ? PointerUpdateKind.LeftButtonPressed : PointerUpdateKind.RightButtonPressed),
KeyModifiers.None));
break;
case WindowsMessages.LBUTTONUP:
case WindowsMessages.RBUTTONUP:
isLeft = msg == WindowsMessages.LBUTTONUP;
this.RaiseEvent(new PointerReleasedEventArgs(
this,
new Pointer(0, PointerType.Mouse, true),
root,
this.TranslatePoint(point, root).Value,
(ulong)Environment.TickCount64,
new PointerPointProperties(isLeft ? RawInputModifiers.LeftMouseButton : RawInputModifiers.RightMouseButton, isLeft ? PointerUpdateKind.LeftButtonReleased : PointerUpdateKind.RightButtonReleased),
KeyModifiers.None,
isLeft ? MouseButton.Left : MouseButton.Right));
break;
case WindowsMessages.MOUSEMOVE:
this.RaiseEvent(new PointerEventArgs(
PointerMovedEvent,
this,
new Pointer(0, PointerType.Mouse, true),
root,
this.TranslatePoint(point, root).Value,
(ulong)Environment.TickCount64,
new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.Other),
KeyModifiers.None));
break;
}
return DefWindowProc(hWnd, msg, wParam, lParam);
}
[SupportedOSPlatform("macos")]
IPlatformHandle CreateMacOs(IPlatformHandle parent)
{
MetalLayer = MetalHelper.GetMetalLayer(out IntPtr nsView, out _updateBoundsCallback);
NsView = nsView;
return new PlatformHandle(nsView, "NSView");
}
void DestroyLinux()
{
X11Window?.Dispose();
}
[SupportedOSPlatform("windows")]
void DestroyWin32(IPlatformHandle handle)
{
DestroyWindow(handle.Handle);
UnregisterClass(_className, GetModuleHandle(null));
}
[SupportedOSPlatform("macos")]
void DestroyMacOS()
{
MetalHelper.DestroyMetalLayer(NsView, MetalLayer);
}
}
}

View File

@@ -1,25 +0,0 @@
using Avalonia.OpenGL;
using SPB.Graphics.OpenGL;
using System;
namespace Ryujinx.Ava.UI.Helpers
{
internal static class IGlContextExtension
{
public static OpenGLContextBase AsOpenGLContextBase(this IGlContext context)
{
var handle = (IntPtr)context.GetType().GetProperty("Handle").GetValue(context);
if (OperatingSystem.IsWindows())
{
return new AvaloniaWglContext(handle);
}
else if (OperatingSystem.IsLinux())
{
return new AvaloniaGlxContext(handle);
}
return null;
}
}
}

View File

@@ -2,7 +2,7 @@ using Avalonia.Utilities;
using System; using System;
using System.Text; using System.Text;
namespace Ryujinx.Ava.UI.Helper namespace Ryujinx.Ava.UI.Helpers
{ {
using AvaLogger = Avalonia.Logging.Logger; using AvaLogger = Avalonia.Logging.Logger;
using AvaLogLevel = Avalonia.Logging.LogEventLevel; using AvaLogLevel = Avalonia.Logging.LogEventLevel;
@@ -20,12 +20,12 @@ namespace Ryujinx.Ava.UI.Helper
{ {
return level switch return level switch
{ {
AvaLogLevel.Verbose => RyuLogger.Trace, AvaLogLevel.Verbose => RyuLogger.Debug,
AvaLogLevel.Debug => RyuLogger.Debug, AvaLogLevel.Debug => RyuLogger.Debug,
AvaLogLevel.Information => RyuLogger.Info, AvaLogLevel.Information => RyuLogger.Debug,
AvaLogLevel.Warning => RyuLogger.Warning, AvaLogLevel.Warning => RyuLogger.Debug,
AvaLogLevel.Error => RyuLogger.Error, AvaLogLevel.Error => RyuLogger.Error,
AvaLogLevel.Fatal => RyuLogger.Notice, AvaLogLevel.Fatal => RyuLogger.Error,
_ => throw new ArgumentOutOfRangeException(nameof(level), level, null) _ => throw new ArgumentOutOfRangeException(nameof(level), level, null)
}; };
} }
@@ -37,34 +37,38 @@ namespace Ryujinx.Ava.UI.Helper
public void Log(AvaLogLevel level, string area, object source, string messageTemplate) public void Log(AvaLogLevel level, string area, object source, string messageTemplate)
{ {
GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(area, messageTemplate, source, null)); GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(level, area, messageTemplate, source, null));
} }
public void Log<T0>(AvaLogLevel level, string area, object source, string messageTemplate, T0 propertyValue0) 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 })); GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(level, area, messageTemplate, source, new object[] { propertyValue0 }));
} }
public void Log<T0, T1>(AvaLogLevel level, string area, object source, string messageTemplate, T0 propertyValue0, T1 propertyValue1) 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 })); GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(level, 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) 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 })); GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(level, area, messageTemplate, source, new object[] { propertyValue0, propertyValue1, propertyValue2 }));
} }
public void Log(AvaLogLevel level, string area, object source, string messageTemplate, params object[] propertyValues) public void Log(AvaLogLevel level, string area, object source, string messageTemplate, params object[] propertyValues)
{ {
GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(area, messageTemplate, source, propertyValues)); GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(level, area, messageTemplate, source, propertyValues));
} }
private static string Format(string area, string template, object source, object[] v) private static string Format(AvaLogLevel level, string area, string template, object source, object[] v)
{ {
var result = new StringBuilder(); var result = new StringBuilder();
var r = new CharacterReader(template.AsSpan()); var r = new CharacterReader(template.AsSpan());
var i = 0; int i = 0;
result.Append('[');
result.Append(level);
result.Append("] ");
result.Append('['); result.Append('[');
result.Append(area); result.Append(area);

View File

@@ -3,7 +3,7 @@ using System.Runtime.Versioning;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Avalonia; using Avalonia;
namespace Ryujinx.Ava.UI.Helper namespace Ryujinx.Ava.UI.Helpers
{ {
public delegate void UpdateBoundsCallbackDelegate(Rect rect); public delegate void UpdateBoundsCallbackDelegate(Rect rect);

View File

@@ -1,52 +0,0 @@
using Avalonia.Platform;
using Silk.NET.Vulkan;
using SPB.Graphics.Vulkan;
using SPB.Platform.GLX;
using SPB.Platform.Metal;
using SPB.Platform.Win32;
using SPB.Platform.X11;
using SPB.Windowing;
using System;
using System.Runtime.Versioning;
namespace Ryujinx.Ava.UI.Helpers
{
public class VulkanEmbeddedWindow : EmbeddedWindow
{
private NativeWindowBase _window;
[SupportedOSPlatform("linux")]
protected override IPlatformHandle CreateLinux(IPlatformHandle parent)
{
X11Window = new GLXWindow(new NativeHandle(X11.DefaultDisplay), new NativeHandle(parent.Handle));
WindowHandle = X11Window.WindowHandle.RawHandle;
X11Display = X11Window.DisplayHandle.RawHandle;
X11Window.Hide();
return new PlatformHandle(WindowHandle, "X11");
}
public SurfaceKHR CreateSurface(Instance instance)
{
if (OperatingSystem.IsWindows())
{
_window = new SimpleWin32Window(new NativeHandle(WindowHandle));
}
else if (OperatingSystem.IsLinux())
{
_window = new SimpleX11Window(new NativeHandle(X11Display), new NativeHandle(WindowHandle));
}
else if (OperatingSystem.IsMacOS())
{
_window = new SimpleMetalWindow(new NativeHandle(NsView), new NativeHandle(MetalLayer));
}
else
{
throw new PlatformNotSupportedException();
}
return new SurfaceKHR((ulong?)VulkanHelper.CreateWindowSurface(instance.Handle, _window));
}
}
}

View File

@@ -70,6 +70,22 @@ namespace Ryujinx.Ava.UI.Helpers
} }
} }
public static IntPtr CreateEmptyCursor()
{
return CreateCursor(IntPtr.Zero, 0, 0, 1, 1, new byte[] { 0xFF }, new byte[] { 0x00 });
}
public static IntPtr CreateArrowCursor()
{
return LoadCursor(IntPtr.Zero, (IntPtr)Cursors.IDC_ARROW);
}
[LibraryImport("user32.dll")]
public static partial IntPtr SetCursor(IntPtr handle);
[LibraryImport("user32.dll")]
public static partial IntPtr CreateCursor(IntPtr hInst, int xHotSpot, int yHotSpot, int nWidth, int nHeight, byte[] pvANDPlane, byte[] pvXORPlane);
[LibraryImport("user32.dll", SetLastError = true, EntryPoint = "RegisterClassExW")] [LibraryImport("user32.dll", SetLastError = true, EntryPoint = "RegisterClassExW")]
public static partial ushort RegisterClassEx(ref WNDCLASSEX param); public static partial ushort RegisterClassEx(ref WNDCLASSEX param);

View File

@@ -1,6 +1,9 @@
using Avalonia.Media;
using Ryujinx.Ava.UI.ViewModels;
namespace Ryujinx.Ava.UI.Models namespace Ryujinx.Ava.UI.Models
{ {
public class ProfileImageModel public class ProfileImageModel : BaseModel
{ {
public ProfileImageModel(string name, byte[] data) public ProfileImageModel(string name, byte[] data)
{ {
@@ -10,5 +13,20 @@ namespace Ryujinx.Ava.UI.Models
public string Name { get; set; } public string Name { get; set; }
public byte[] Data { get; set; } public byte[] Data { get; set; }
private SolidColorBrush _backgroundColor = new(Colors.White);
public SolidColorBrush BackgroundColor
{
get
{
return _backgroundColor;
}
set
{
_backgroundColor = value;
OnPropertyChanged();
}
}
} }
} }

View File

@@ -1,15 +1,8 @@
using LibHac;
using LibHac.Fs; using LibHac.Fs;
using LibHac.Fs.Shim;
using LibHac.Ncm; using LibHac.Ncm;
using Ryujinx.Ava.Common;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.UI.Windows;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.Ui.App.Common;
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@@ -19,10 +12,8 @@ namespace Ryujinx.Ava.UI.Models
{ {
public class SaveModel : BaseModel public class SaveModel : BaseModel
{ {
private readonly HorizonClient _horizonClient;
private long _size; private long _size;
public Action DeleteAction { get; set; }
public ulong SaveId { get; } public ulong SaveId { get; }
public ProgramId TitleId { get; } public ProgramId TitleId { get; }
public string TitleIdString => $"{TitleId.Value:X16}"; public string TitleIdString => $"{TitleId.Value:X16}";
@@ -45,11 +36,29 @@ namespace Ryujinx.Ava.UI.Models
public bool SizeAvailable { get; set; } public bool SizeAvailable { get; set; }
public string SizeString => $"{((float)_size * 0.000000954):0.###}MB"; public string SizeString => GetSizeString();
public SaveModel(SaveDataInfo info, HorizonClient horizonClient, VirtualFileSystem virtualFileSystem) private string GetSizeString()
{
const int scale = 1024;
string[] orders = { "GiB", "MiB", "KiB" };
long max = (long)Math.Pow(scale, orders.Length);
foreach (string order in orders)
{
if (Size > max)
{
return $"{decimal.Divide(Size, max):##.##} {order}";
}
max /= scale;
}
return "0 KiB";
}
public SaveModel(SaveDataInfo info, VirtualFileSystem virtualFileSystem)
{ {
_horizonClient = horizonClient;
SaveId = info.SaveDataId; SaveId = info.SaveDataId;
TitleId = info.ProgramId; TitleId = info.ProgramId;
UserId = info.UserId; UserId = info.UserId;
@@ -99,25 +108,5 @@ namespace Ryujinx.Ava.UI.Models
}); });
} }
public void OpenLocation()
{
ApplicationHelper.OpenSaveDir(SaveId);
}
public async void Delete()
{
var result = await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance[LocaleKeys.DeleteUserSave],
LocaleManager.Instance[LocaleKeys.IrreversibleActionNote],
LocaleManager.Instance[LocaleKeys.InputDialogYes],
LocaleManager.Instance[LocaleKeys.InputDialogNo], "");
if (result == UserResult.Yes)
{
_horizonClient.Fs.DeleteSaveData(SaveDataSpaceId.User, SaveId);
DeleteAction?.Invoke();
}
}
} }
} }

View File

@@ -7,10 +7,12 @@ namespace Ryujinx.Ava.UI.Models
public class TempProfile : BaseModel public class TempProfile : BaseModel
{ {
private readonly UserProfile _profile; private readonly UserProfile _profile;
private byte[] _image = null; private byte[] _image;
private string _name = String.Empty; private string _name = String.Empty;
private UserId _userId; private UserId _userId;
public uint MaxProfileNameLength => 0x20;
public byte[] Image public byte[] Image
{ {
get => _image; get => _image;
@@ -28,9 +30,12 @@ namespace Ryujinx.Ava.UI.Models
{ {
_userId = value; _userId = value;
OnPropertyChanged(); OnPropertyChanged();
OnPropertyChanged(nameof(UserIdString));
} }
} }
public string UserIdString => _userId.ToString();
public string Name public string Name
{ {
get => _name; get => _name;
@@ -52,7 +57,5 @@ namespace Ryujinx.Ava.UI.Models
UserId = profile.UserId; UserId = profile.UserId;
} }
} }
public TempProfile(){}
} }
} }

View File

@@ -3,23 +3,17 @@ using Ryujinx.Ava.Common.Locale;
namespace Ryujinx.Ava.UI.Models namespace Ryujinx.Ava.UI.Models
{ {
internal class TitleUpdateModel public class TitleUpdateModel
{ {
public bool IsEnabled { get; set; }
public bool IsNoUpdate { get; }
public ApplicationControlProperty Control { get; } public ApplicationControlProperty Control { get; }
public string Path { get; } public string Path { get; }
public string Label => IsNoUpdate public string Label => string.Format(LocaleManager.Instance[LocaleKeys.TitleUpdateVersionLabel], Control.DisplayVersionString.ToString());
? LocaleManager.Instance[LocaleKeys.NoUpdate]
: string.Format(LocaleManager.Instance[LocaleKeys.TitleUpdateVersionLabel], Control.DisplayVersionString.ToString(),
Path);
public TitleUpdateModel(ApplicationControlProperty control, string path, bool isNoUpdate = false) public TitleUpdateModel(ApplicationControlProperty control, string path)
{ {
Control = control; Control = control;
Path = path; Path = path;
IsNoUpdate = isNoUpdate;
} }
} }
} }

View File

@@ -1,5 +1,7 @@
using Avalonia.Media;
using Ryujinx.Ava.UI.Controls; using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Views.User;
using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.HLE.HOS.Services.Account.Acc;
using Profile = Ryujinx.HLE.HOS.Services.Account.Acc.UserProfile; using Profile = Ryujinx.HLE.HOS.Services.Account.Acc.UserProfile;
@@ -12,6 +14,8 @@ namespace Ryujinx.Ava.UI.Models
private byte[] _image; private byte[] _image;
private string _name; private string _name;
private UserId _userId; private UserId _userId;
private bool _isPointerOver;
private IBrush _backgroundColor;
public byte[] Image public byte[] Image
{ {
@@ -43,27 +47,57 @@ namespace Ryujinx.Ava.UI.Models
} }
} }
public bool IsPointerOver
{
get => _isPointerOver;
set
{
_isPointerOver = value;
OnPropertyChanged();
}
}
public IBrush BackgroundColor
{
get => _backgroundColor;
set
{
_backgroundColor = value;
OnPropertyChanged();
}
}
public UserProfile(Profile profile, NavigationDialogHost owner) public UserProfile(Profile profile, NavigationDialogHost owner)
{ {
_profile = profile; _profile = profile;
_owner = owner; _owner = owner;
UpdateBackground();
Image = profile.Image; Image = profile.Image;
Name = profile.Name; Name = profile.Name;
UserId = profile.UserId; UserId = profile.UserId;
} }
public bool IsOpened => _profile.AccountState == AccountState.Open;
public void UpdateState() public void UpdateState()
{ {
OnPropertyChanged(nameof(IsOpened)); UpdateBackground();
OnPropertyChanged(nameof(Name)); OnPropertyChanged(nameof(Name));
} }
private void UpdateBackground()
{
Avalonia.Application.Current.Styles.TryGetResource("ControlFillColorSecondary", out object color);
if (color is not null)
{
BackgroundColor = _profile.AccountState == AccountState.Open ? new SolidColorBrush((Color)color) : Brushes.Transparent;
}
}
public void Recover(UserProfile userProfile) public void Recover(UserProfile userProfile)
{ {
_owner.Navigate(typeof(UserEditor), (_owner, userProfile, true)); _owner.Navigate(typeof(UserEditorView), (_owner, userProfile, true));
} }
} }
} }

View File

@@ -0,0 +1,259 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Platform;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Common.Configuration;
using Ryujinx.Ui.Common.Configuration;
using SPB.Graphics;
using SPB.Platform;
using SPB.Platform.GLX;
using SPB.Platform.X11;
using SPB.Windowing;
using System;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop;
namespace Ryujinx.Ava.UI.Renderer
{
public class EmbeddedWindow : NativeControlHost
{
private WindowProc _wndProcDelegate;
private string _className;
protected GLXWindow X11Window { get; set; }
protected IntPtr WindowHandle { get; set; }
protected IntPtr X11Display { get; set; }
protected IntPtr NsView { get; set; }
protected IntPtr MetalLayer { get; set; }
private UpdateBoundsCallbackDelegate _updateBoundsCallback;
public event EventHandler<IntPtr> WindowCreated;
public event EventHandler<Size> SizeChanged;
public EmbeddedWindow()
{
this.GetObservable(BoundsProperty).Subscribe(StateChanged);
Initialized += OnNativeEmbeddedWindowCreated;
}
public virtual void OnWindowCreated() { }
protected virtual void OnWindowDestroyed() { }
protected virtual void OnWindowDestroying()
{
WindowHandle = IntPtr.Zero;
X11Display = IntPtr.Zero;
NsView = IntPtr.Zero;
MetalLayer = IntPtr.Zero;
}
private void OnNativeEmbeddedWindowCreated(object sender, EventArgs e)
{
OnWindowCreated();
Task.Run(() =>
{
WindowCreated?.Invoke(this, WindowHandle);
});
}
private void StateChanged(Rect rect)
{
SizeChanged?.Invoke(this, rect.Size);
_updateBoundsCallback?.Invoke(rect);
}
protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle control)
{
if (OperatingSystem.IsLinux())
{
return CreateLinux(control);
}
else if (OperatingSystem.IsWindows())
{
return CreateWin32(control);
}
else if (OperatingSystem.IsMacOS())
{
return CreateMacOS();
}
return base.CreateNativeControlCore(control);
}
protected override void DestroyNativeControlCore(IPlatformHandle control)
{
OnWindowDestroying();
if (OperatingSystem.IsLinux())
{
DestroyLinux();
}
else if (OperatingSystem.IsWindows())
{
DestroyWin32(control);
}
else if (OperatingSystem.IsMacOS())
{
DestroyMacOS();
}
else
{
base.DestroyNativeControlCore(control);
}
OnWindowDestroyed();
}
[SupportedOSPlatform("linux")]
private IPlatformHandle CreateLinux(IPlatformHandle control)
{
if (ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.Vulkan)
{
X11Window = new GLXWindow(new NativeHandle(X11.DefaultDisplay), new NativeHandle(control.Handle));
X11Window.Hide();
}
else
{
X11Window = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100) as GLXWindow;
}
WindowHandle = X11Window.WindowHandle.RawHandle;
X11Display = X11Window.DisplayHandle.RawHandle;
return new PlatformHandle(WindowHandle, "X11");
}
[SupportedOSPlatform("windows")]
IPlatformHandle CreateWin32(IPlatformHandle control)
{
_className = "NativeWindow-" + Guid.NewGuid();
_wndProcDelegate = delegate (IntPtr hWnd, WindowsMessages msg, IntPtr wParam, IntPtr lParam)
{
if (VisualRoot != null)
{
Point rootVisualPosition = this.TranslatePoint(new Point((long)lParam & 0xFFFF, (long)lParam >> 16 & 0xFFFF), VisualRoot).Value;
Pointer pointer = new(0, PointerType.Mouse, true);
switch (msg)
{
case WindowsMessages.LBUTTONDOWN:
case WindowsMessages.RBUTTONDOWN:
{
bool isLeft = msg == WindowsMessages.LBUTTONDOWN;
RawInputModifiers pointerPointModifier = isLeft ? RawInputModifiers.LeftMouseButton : RawInputModifiers.RightMouseButton;
PointerPointProperties properties = new(pointerPointModifier, isLeft ? PointerUpdateKind.LeftButtonPressed : PointerUpdateKind.RightButtonPressed);
var evnt = new PointerPressedEventArgs(
this,
pointer,
VisualRoot,
rootVisualPosition,
(ulong)Environment.TickCount64,
properties,
KeyModifiers.None);
RaiseEvent(evnt);
break;
}
case WindowsMessages.LBUTTONUP:
case WindowsMessages.RBUTTONUP:
{
bool isLeft = msg == WindowsMessages.LBUTTONUP;
RawInputModifiers pointerPointModifier = isLeft ? RawInputModifiers.LeftMouseButton : RawInputModifiers.RightMouseButton;
PointerPointProperties properties = new(pointerPointModifier, isLeft ? PointerUpdateKind.LeftButtonReleased : PointerUpdateKind.RightButtonReleased);
var evnt = new PointerReleasedEventArgs(
this,
pointer,
VisualRoot,
rootVisualPosition,
(ulong)Environment.TickCount64,
properties,
KeyModifiers.None,
isLeft ? MouseButton.Left : MouseButton.Right);
RaiseEvent(evnt);
break;
}
case WindowsMessages.MOUSEMOVE:
{
var evnt = new PointerEventArgs(
PointerMovedEvent,
this,
pointer,
VisualRoot,
rootVisualPosition,
(ulong)Environment.TickCount64,
new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.Other),
KeyModifiers.None);
RaiseEvent(evnt);
break;
}
}
}
return DefWindowProc(hWnd, msg, wParam, lParam);
};
WNDCLASSEX wndClassEx = new()
{
cbSize = Marshal.SizeOf<WNDCLASSEX>(),
hInstance = GetModuleHandle(null),
lpfnWndProc = Marshal.GetFunctionPointerForDelegate(_wndProcDelegate),
style = ClassStyles.CS_OWNDC,
lpszClassName = Marshal.StringToHGlobalUni(_className),
hCursor = CreateArrowCursor()
};
RegisterClassEx(ref wndClassEx);
WindowHandle = CreateWindowEx(0, _className, "NativeWindow", WindowStyles.WS_CHILD, 0, 0, 640, 480, control.Handle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
Marshal.FreeHGlobal(wndClassEx.lpszClassName);
return new PlatformHandle(WindowHandle, "HWND");
}
[SupportedOSPlatform("macos")]
IPlatformHandle CreateMacOS()
{
MetalLayer = MetalHelper.GetMetalLayer(out IntPtr nsView, out _updateBoundsCallback);
NsView = nsView;
return new PlatformHandle(nsView, "NSView");
}
[SupportedOSPlatform("Linux")]
void DestroyLinux()
{
X11Window?.Dispose();
}
[SupportedOSPlatform("windows")]
void DestroyWin32(IPlatformHandle handle)
{
DestroyWindow(handle.Handle);
UnregisterClass(_className, GetModuleHandle(null));
}
[SupportedOSPlatform("macos")]
void DestroyMacOS()
{
MetalHelper.DestroyMetalLayer(NsView, MetalLayer);
}
}
}

View File

@@ -1,5 +1,8 @@
using OpenTK.Graphics.OpenGL; using OpenTK.Graphics.OpenGL;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.OpenGL;
using Ryujinx.Ui.Common.Configuration;
using SPB.Graphics; using SPB.Graphics;
using SPB.Graphics.OpenGL; using SPB.Graphics.OpenGL;
using SPB.Platform; using SPB.Platform;
@@ -7,26 +10,20 @@ using SPB.Platform.WGL;
using SPB.Windowing; using SPB.Windowing;
using System; using System;
namespace Ryujinx.Ava.UI.Helpers namespace Ryujinx.Ava.UI.Renderer
{ {
public class OpenGLEmbeddedWindow : EmbeddedWindow public class EmbeddedWindowOpenGL : EmbeddedWindow
{ {
private readonly int _major;
private readonly int _minor;
private readonly GraphicsDebugLevel _graphicsDebugLevel;
private SwappableNativeWindowBase _window; private SwappableNativeWindowBase _window;
public OpenGLContextBase Context { get; set; } public OpenGLContextBase Context { get; set; }
public OpenGLEmbeddedWindow(int major, int minor, GraphicsDebugLevel graphicsDebugLevel) public EmbeddedWindowOpenGL() { }
{
_major = major;
_minor = minor;
_graphicsDebugLevel = graphicsDebugLevel;
}
protected override void OnWindowDestroying() protected override void OnWindowDestroying()
{ {
Context.Dispose(); Context.Dispose();
base.OnWindowDestroying(); base.OnWindowDestroying();
} }
@@ -48,19 +45,20 @@ namespace Ryujinx.Ava.UI.Helpers
} }
var flags = OpenGLContextFlags.Compat; var flags = OpenGLContextFlags.Compat;
if (_graphicsDebugLevel != GraphicsDebugLevel.None) if (ConfigurationState.Instance.Logger.GraphicsDebugLevel != GraphicsDebugLevel.None)
{ {
flags |= OpenGLContextFlags.Debug; flags |= OpenGLContextFlags.Debug;
} }
Context = PlatformHelper.CreateOpenGLContext(FramebufferFormat.Default, _major, _minor, flags); var graphicsMode = Environment.OSVersion.Platform == PlatformID.Unix ? new FramebufferFormat(new ColorFormat(8, 8, 8, 0), 16, 0, ColorFormat.Zero, 0, 2, false) : FramebufferFormat.Default;
Context = PlatformHelper.CreateOpenGLContext(graphicsMode, 3, 3, flags);
Context.Initialize(_window); Context.Initialize(_window);
Context.MakeCurrent(_window); Context.MakeCurrent(_window);
var bindingsContext = new OpenToolkitBindingsContext(Context.GetProcAddress); GL.LoadBindings(new OpenTKBindingsContext(Context.GetProcAddress));
GL.LoadBindings(bindingsContext);
Context.MakeCurrent(null); Context.MakeCurrent(null);
} }
@@ -76,7 +74,14 @@ namespace Ryujinx.Ava.UI.Helpers
public void SwapBuffers() public void SwapBuffers()
{ {
_window.SwapBuffers(); _window?.SwapBuffers();
}
public void InitializeBackgroundContext(IRenderer renderer)
{
(renderer as OpenGLRenderer)?.InitializeBackgroundContext(SPBOpenGLContext.CreateBackgroundContext(Context));
MakeCurrent();
} }
} }
} }

View File

@@ -0,0 +1,42 @@
using Silk.NET.Vulkan;
using SPB.Graphics.Vulkan;
using SPB.Platform.Metal;
using SPB.Platform.Win32;
using SPB.Platform.X11;
using SPB.Windowing;
using System;
namespace Ryujinx.Ava.UI.Renderer
{
public class EmbeddedWindowVulkan : EmbeddedWindow
{
public SurfaceKHR CreateSurface(Instance instance)
{
NativeWindowBase nativeWindowBase;
if (OperatingSystem.IsWindows())
{
nativeWindowBase = new SimpleWin32Window(new NativeHandle(WindowHandle));
}
else if (OperatingSystem.IsLinux())
{
nativeWindowBase = new SimpleX11Window(new NativeHandle(X11Display), new NativeHandle(WindowHandle));
}
else if (OperatingSystem.IsMacOS())
{
nativeWindowBase = new SimpleMetalWindow(new NativeHandle(NsView), new NativeHandle(MetalLayer));
}
else
{
throw new PlatformNotSupportedException();
}
return new SurfaceKHR((ulong?)VulkanHelper.CreateWindowSurface(instance.Handle, nativeWindowBase));
}
public SurfaceKHR CreateSurface(Instance instance, Vk api)
{
return CreateSurface(instance);
}
}
}

View File

@@ -1,13 +1,13 @@
using OpenTK; using OpenTK;
using System; using System;
namespace Ryujinx.Ava.UI.Helpers namespace Ryujinx.Ava.UI.Renderer
{ {
internal class OpenToolkitBindingsContext : IBindingsContext internal class OpenTKBindingsContext : IBindingsContext
{ {
private readonly Func<string, IntPtr> _getProcAddress; private readonly Func<string, IntPtr> _getProcAddress;
public OpenToolkitBindingsContext(Func<string, IntPtr> getProcAddress) public OpenTKBindingsContext(Func<string, IntPtr> getProcAddress)
{ {
_getProcAddress = getProcAddress; _getProcAddress = getProcAddress;
} }

View File

@@ -6,6 +6,6 @@
mc:Ignorable="d" mc:Ignorable="d"
d:DesignWidth="800" d:DesignWidth="800"
d:DesignHeight="450" d:DesignHeight="450"
x:Class="Ryujinx.Ava.UI.Controls.RendererHost" x:Class="Ryujinx.Ava.UI.Renderer.RendererHost"
Focusable="True"> Focusable="True">
</UserControl> </UserControl>

View File

@@ -0,0 +1,68 @@
using Avalonia;
using Avalonia.Controls;
using Ryujinx.Common.Configuration;
using Ryujinx.Ui.Common.Configuration;
using System;
namespace Ryujinx.Ava.UI.Renderer
{
public partial class RendererHost : UserControl, IDisposable
{
public readonly EmbeddedWindow EmbeddedWindow;
public event EventHandler<EventArgs> WindowCreated;
public event Action<object, Size> SizeChanged;
public RendererHost()
{
InitializeComponent();
if (ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.OpenGl)
{
EmbeddedWindow = new EmbeddedWindowOpenGL();
}
else
{
EmbeddedWindow = new EmbeddedWindowVulkan();
}
Initialize();
}
private void Initialize()
{
EmbeddedWindow.WindowCreated += CurrentWindow_WindowCreated;
EmbeddedWindow.SizeChanged += CurrentWindow_SizeChanged;
Content = EmbeddedWindow;
}
public void Dispose()
{
if (EmbeddedWindow != null)
{
EmbeddedWindow.WindowCreated -= CurrentWindow_WindowCreated;
EmbeddedWindow.SizeChanged -= CurrentWindow_SizeChanged;
}
GC.SuppressFinalize(this);
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
Dispose();
}
private void CurrentWindow_SizeChanged(object sender, Size e)
{
SizeChanged?.Invoke(sender, e);
}
private void CurrentWindow_WindowCreated(object sender, IntPtr e)
{
WindowCreated?.Invoke(this, EventArgs.Empty);
}
}
}

View File

@@ -5,12 +5,12 @@ using SPB.Graphics.OpenGL;
using SPB.Platform; using SPB.Platform;
using SPB.Windowing; using SPB.Windowing;
namespace Ryujinx.Ava.UI.Helpers namespace Ryujinx.Ava.UI.Renderer
{ {
class SPBOpenGLContext : IOpenGLContext class SPBOpenGLContext : IOpenGLContext
{ {
private OpenGLContextBase _context; private readonly OpenGLContextBase _context;
private NativeWindowBase _window; private readonly NativeWindowBase _window;
private SPBOpenGLContext(OpenGLContextBase context, NativeWindowBase window) private SPBOpenGLContext(OpenGLContextBase context, NativeWindowBase window)
{ {
@@ -37,7 +37,7 @@ namespace Ryujinx.Ava.UI.Helpers
context.Initialize(window); context.Initialize(window);
context.MakeCurrent(window); context.MakeCurrent(window);
GL.LoadBindings(new OpenToolkitBindingsContext(context.GetProcAddress)); GL.LoadBindings(new OpenTKBindingsContext(context.GetProcAddress));
context.MakeCurrent(null); context.MakeCurrent(null);

View File

@@ -435,7 +435,7 @@ namespace Ryujinx.Ava.UI.ViewModels
if (str.Length > MaxSize) if (str.Length > MaxSize)
{ {
return str.Substring(0, MaxSize - Ellipsis.Length) + Ellipsis; return $"{str.AsSpan(0, MaxSize - Ellipsis.Length)}{Ellipsis}";
} }
return str; return str;

View File

@@ -13,6 +13,7 @@ using Ryujinx.Ava.Input;
using Ryujinx.Ava.UI.Controls; using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.Renderer;
using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
@@ -870,7 +871,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public Action<bool> SwitchToGameControl { get; private set; } public Action<bool> SwitchToGameControl { get; private set; }
public Action<Control> SetMainContent { get; private set; } public Action<Control> SetMainContent { get; private set; }
public TopLevel TopLevel { get; private set; } public TopLevel TopLevel { get; private set; }
public RendererHost RendererControl { get; private set; } public RendererHost RendererHostControl { get; private set; }
public bool IsClosing { get; set; } public bool IsClosing { get; set; }
public LibHacHorizonManager LibHacHorizonManager { get; internal set; } public LibHacHorizonManager LibHacHorizonManager { get; internal set; }
public IHostUiHandler UiHandler { get; internal set; } public IHostUiHandler UiHandler { get; internal set; }
@@ -1144,7 +1145,7 @@ namespace Ryujinx.Ava.UI.ViewModels
private void InitializeGame() private void InitializeGame()
{ {
RendererControl.RendererInitialized += GlRenderer_Created; RendererHostControl.WindowCreated += RendererHost_Created;
AppHost.StatusUpdatedEvent += Update_StatusBar; AppHost.StatusUpdatedEvent += Update_StatusBar;
AppHost.AppExit += AppHost_AppExit; AppHost.AppExit += AppHost_AppExit;
@@ -1203,7 +1204,7 @@ namespace Ryujinx.Ava.UI.ViewModels
} }
} }
private void GlRenderer_Created(object sender, EventArgs e) private void RendererHost_Created(object sender, EventArgs e)
{ {
ShowLoading(false); ShowLoading(false);
@@ -1601,13 +1602,9 @@ namespace Ryujinx.Ava.UI.ViewModels
public async void OpenTitleUpdateManager() public async void OpenTitleUpdateManager()
{ {
ApplicationData selection = SelectedApplication; if (SelectedApplication != null)
if (selection != null)
{ {
if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) await TitleUpdateWindow.Show(VirtualFileSystem, ulong.Parse(SelectedApplication.TitleId, NumberStyles.HexNumber), SelectedApplication.TitleName);
{
await new TitleUpdateWindow(VirtualFileSystem, ulong.Parse(selection.TitleId, NumberStyles.HexNumber), selection.TitleName).ShowDialog(desktop.MainWindow);
}
} }
} }
@@ -1735,18 +1732,10 @@ namespace Ryujinx.Ava.UI.ViewModels
PrepareLoadScreen(); PrepareLoadScreen();
RendererControl = new RendererHost(ConfigurationState.Instance.Logger.GraphicsDebugLevel); RendererHostControl = new RendererHost();
if (ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.OpenGl)
{
RendererControl.CreateOpenGL();
}
else
{
RendererControl.CreateVulkan();
}
AppHost = new AppHost( AppHost = new AppHost(
RendererControl, RendererHostControl,
InputManager, InputManager,
path, path,
VirtualFileSystem, VirtualFileSystem,
@@ -1787,9 +1776,9 @@ namespace Ryujinx.Ava.UI.ViewModels
{ {
SwitchToGameControl(startFullscreen); SwitchToGameControl(startFullscreen);
SetMainContent(RendererControl); SetMainContent(RendererHostControl);
RendererControl.Focus(); RendererHostControl.Focus();
}); });
} }
@@ -1857,8 +1846,8 @@ namespace Ryujinx.Ava.UI.ViewModels
HandleRelaunch(); HandleRelaunch();
}); });
RendererControl.RendererInitialized -= GlRenderer_Created; RendererHostControl.WindowCreated -= RendererHost_Created;
RendererControl = null; RendererHostControl = null;
SelectedIcon = null; SelectedIcon = null;
@@ -1918,7 +1907,7 @@ namespace Ryujinx.Ava.UI.ViewModels
LocaleManager.Instance[LocaleKeys.InputDialogNo], LocaleManager.Instance[LocaleKeys.InputDialogNo],
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
if (result != UserResult.Yes) if (result == UserResult.Yes)
{ {
ConfigurationState.Instance.Logger.EnableTrace.Value = false; ConfigurationState.Instance.Logger.EnableTrace.Value = false;
@@ -1938,7 +1927,7 @@ namespace Ryujinx.Ava.UI.ViewModels
LocaleManager.Instance[LocaleKeys.InputDialogNo], LocaleManager.Instance[LocaleKeys.InputDialogNo],
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
if (result != UserResult.Yes) if (result == UserResult.Yes)
{ {
ConfigurationState.Instance.Graphics.ShadersDumpPath.Value = ""; ConfigurationState.Instance.Graphics.ShadersDumpPath.Value = "";

View File

@@ -151,7 +151,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool IsSoundIoEnabled { get; set; } public bool IsSoundIoEnabled { get; set; }
public bool IsSDL2Enabled { get; set; } public bool IsSDL2Enabled { get; set; }
public bool EnableCustomTheme { get; set; } public bool EnableCustomTheme { get; set; }
public bool IsCustomResolutionScaleActive => _resolutionScale == 0; public bool IsCustomResolutionScaleActive => _resolutionScale == 4;
public bool IsVulkanSelected => GraphicsBackendIndex == 0; public bool IsVulkanSelected => GraphicsBackendIndex == 0;
public string TimeZone { get; set; } public string TimeZone { get; set; }
@@ -311,25 +311,66 @@ namespace Ryujinx.Ava.UI.ViewModels
{ {
ConfigurationState config = ConfigurationState.Instance; ConfigurationState config = ConfigurationState.Instance;
GameDirectories.Clear(); // User Interface
GameDirectories.AddRange(config.Ui.GameDirs.Value);
EnableDiscordIntegration = config.EnableDiscordIntegration; EnableDiscordIntegration = config.EnableDiscordIntegration;
CheckUpdatesOnStart = config.CheckUpdatesOnStart; CheckUpdatesOnStart = config.CheckUpdatesOnStart;
ShowConfirmExit = config.ShowConfirmExit; ShowConfirmExit = config.ShowConfirmExit;
HideCursorOnIdle = config.HideCursorOnIdle; HideCursorOnIdle = config.HideCursorOnIdle;
GameDirectories.Clear();
GameDirectories.AddRange(config.Ui.GameDirs.Value);
EnableCustomTheme = config.Ui.EnableCustomTheme;
CustomThemePath = config.Ui.CustomThemePath;
BaseStyleIndex = config.Ui.BaseStyle == "Light" ? 0 : 1;
// Input
EnableDockedMode = config.System.EnableDockedMode; EnableDockedMode = config.System.EnableDockedMode;
EnableKeyboard = config.Hid.EnableKeyboard; EnableKeyboard = config.Hid.EnableKeyboard;
EnableMouse = config.Hid.EnableMouse; EnableMouse = config.Hid.EnableMouse;
// Keyboard Hotkeys
KeyboardHotkeys = config.Hid.Hotkeys.Value;
// System
Region = (int)config.System.Region.Value;
Language = (int)config.System.Language.Value;
TimeZone = config.System.TimeZone;
DateTime dateTimeOffset = DateTime.Now.AddSeconds(config.System.SystemTimeOffset);
DateOffset = dateTimeOffset.Date;
TimeOffset = dateTimeOffset.TimeOfDay;
EnableVsync = config.Graphics.EnableVsync; EnableVsync = config.Graphics.EnableVsync;
EnablePptc = config.System.EnablePtc;
EnableInternetAccess = config.System.EnableInternetAccess;
EnableFsIntegrityChecks = config.System.EnableFsIntegrityChecks; EnableFsIntegrityChecks = config.System.EnableFsIntegrityChecks;
IgnoreMissingServices = config.System.IgnoreMissingServices;
ExpandDramSize = config.System.ExpandRam; ExpandDramSize = config.System.ExpandRam;
IgnoreMissingServices = config.System.IgnoreMissingServices;
// CPU
EnablePptc = config.System.EnablePtc;
MemoryMode = (int)config.System.MemoryManagerMode.Value;
// Graphics
GraphicsBackendIndex = (int)config.Graphics.GraphicsBackend.Value;
PreferredGpuIndex = _gpuIds.Contains(config.Graphics.PreferredGpu) ? _gpuIds.IndexOf(config.Graphics.PreferredGpu) : 0;
EnableShaderCache = config.Graphics.EnableShaderCache; EnableShaderCache = config.Graphics.EnableShaderCache;
EnableTextureRecompression = config.Graphics.EnableTextureRecompression; EnableTextureRecompression = config.Graphics.EnableTextureRecompression;
EnableMacroHLE = config.Graphics.EnableMacroHLE; EnableMacroHLE = config.Graphics.EnableMacroHLE;
ResolutionScale = config.Graphics.ResScale == -1 ? 4 : config.Graphics.ResScale - 1;
CustomResolutionScale = config.Graphics.ResScaleCustom;
MaxAnisotropy = config.Graphics.MaxAnisotropy == -1 ? 0 : (int)(MathF.Log2(config.Graphics.MaxAnisotropy));
AspectRatio = (int)config.Graphics.AspectRatio.Value;
GraphicsBackendMultithreadingIndex = (int)config.Graphics.BackendThreading.Value;
ShaderDumpPath = config.Graphics.ShadersDumpPath;
// Audio
AudioBackend = (int)config.System.AudioBackend.Value;
Volume = config.System.AudioVolume * 100;
// Network
EnableInternetAccess = config.System.EnableInternetAccess;
// Logging
EnableFileLog = config.Logger.EnableFileLog; EnableFileLog = config.Logger.EnableFileLog;
EnableStub = config.Logger.EnableStub; EnableStub = config.Logger.EnableStub;
EnableInfo = config.Logger.EnableInfo; EnableInfo = config.Logger.EnableInfo;
@@ -339,94 +380,69 @@ namespace Ryujinx.Ava.UI.ViewModels
EnableGuest = config.Logger.EnableGuest; EnableGuest = config.Logger.EnableGuest;
EnableDebug = config.Logger.EnableDebug; EnableDebug = config.Logger.EnableDebug;
EnableFsAccessLog = config.Logger.EnableFsAccessLog; EnableFsAccessLog = config.Logger.EnableFsAccessLog;
EnableCustomTheme = config.Ui.EnableCustomTheme;
Volume = config.System.AudioVolume * 100;
GraphicsBackendMultithreadingIndex = (int)config.Graphics.BackendThreading.Value;
OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value;
TimeZone = config.System.TimeZone;
ShaderDumpPath = config.Graphics.ShadersDumpPath;
CustomThemePath = config.Ui.CustomThemePath;
BaseStyleIndex = config.Ui.BaseStyle == "Light" ? 0 : 1;
GraphicsBackendIndex = (int)config.Graphics.GraphicsBackend.Value;
PreferredGpuIndex = _gpuIds.Contains(config.Graphics.PreferredGpu) ? _gpuIds.IndexOf(config.Graphics.PreferredGpu) : 0;
Language = (int)config.System.Language.Value;
Region = (int)config.System.Region.Value;
FsGlobalAccessLogMode = config.System.FsGlobalAccessLogMode; FsGlobalAccessLogMode = config.System.FsGlobalAccessLogMode;
AudioBackend = (int)config.System.AudioBackend.Value; OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value;
MemoryMode = (int)config.System.MemoryManagerMode.Value;
float anisotropy = config.Graphics.MaxAnisotropy;
MaxAnisotropy = anisotropy == -1 ? 0 : (int)(MathF.Log2(anisotropy));
AspectRatio = (int)config.Graphics.AspectRatio.Value;
int resolution = config.Graphics.ResScale;
ResolutionScale = resolution == -1 ? 0 : resolution;
CustomResolutionScale = config.Graphics.ResScaleCustom;
DateTime dateTimeOffset = DateTime.Now.AddSeconds(config.System.SystemTimeOffset);
DateOffset = dateTimeOffset.Date;
TimeOffset = dateTimeOffset.TimeOfDay;
KeyboardHotkeys = config.Hid.Hotkeys.Value;
} }
public void SaveSettings() public void SaveSettings()
{ {
ConfigurationState config = ConfigurationState.Instance; ConfigurationState config = ConfigurationState.Instance;
// User Interface
config.EnableDiscordIntegration.Value = EnableDiscordIntegration;
config.CheckUpdatesOnStart.Value = CheckUpdatesOnStart;
config.ShowConfirmExit.Value = ShowConfirmExit;
config.HideCursorOnIdle.Value = HideCursorOnIdle;
if (_directoryChanged) if (_directoryChanged)
{ {
List<string> gameDirs = new List<string>(GameDirectories); List<string> gameDirs = new(GameDirectories);
config.Ui.GameDirs.Value = gameDirs; config.Ui.GameDirs.Value = gameDirs;
} }
config.Ui.EnableCustomTheme.Value = EnableCustomTheme;
config.Ui.CustomThemePath.Value = CustomThemePath;
config.Ui.BaseStyle.Value = BaseStyleIndex == 0 ? "Light" : "Dark";
// Input
config.System.EnableDockedMode.Value = EnableDockedMode;
config.Hid.EnableKeyboard.Value = EnableKeyboard;
config.Hid.EnableMouse.Value = EnableMouse;
// Keyboard Hotkeys
config.Hid.Hotkeys.Value = KeyboardHotkeys;
// System
config.System.Region.Value = (Region)Region;
config.System.Language.Value = (Language)Language;
if (_validTzRegions.Contains(TimeZone)) if (_validTzRegions.Contains(TimeZone))
{ {
config.System.TimeZone.Value = TimeZone; config.System.TimeZone.Value = TimeZone;
} }
config.Logger.EnableError.Value = EnableError; TimeSpan systemTimeOffset = DateOffset - DateTime.Now;
config.Logger.EnableTrace.Value = EnableTrace;
config.Logger.EnableWarn.Value = EnableWarn; config.System.SystemTimeOffset.Value = systemTimeOffset.Seconds;
config.Logger.EnableInfo.Value = EnableInfo;
config.Logger.EnableStub.Value = EnableStub;
config.Logger.EnableDebug.Value = EnableDebug;
config.Logger.EnableGuest.Value = EnableGuest;
config.Logger.EnableFsAccessLog.Value = EnableFsAccessLog;
config.Logger.EnableFileLog.Value = EnableFileLog;
config.Logger.GraphicsDebugLevel.Value = (GraphicsDebugLevel)OpenglDebugLevel;
config.System.EnableDockedMode.Value = EnableDockedMode;
config.EnableDiscordIntegration.Value = EnableDiscordIntegration;
config.CheckUpdatesOnStart.Value = CheckUpdatesOnStart;
config.ShowConfirmExit.Value = ShowConfirmExit;
config.HideCursorOnIdle.Value = HideCursorOnIdle;
config.Graphics.EnableVsync.Value = EnableVsync; config.Graphics.EnableVsync.Value = EnableVsync;
config.System.EnableFsIntegrityChecks.Value = EnableFsIntegrityChecks;
config.System.ExpandRam.Value = ExpandDramSize;
config.System.IgnoreMissingServices.Value = IgnoreMissingServices;
// CPU
config.System.EnablePtc.Value = EnablePptc;
config.System.MemoryManagerMode.Value = (MemoryManagerMode)MemoryMode;
// Graphics
config.Graphics.GraphicsBackend.Value = (GraphicsBackend)GraphicsBackendIndex;
config.Graphics.PreferredGpu.Value = _gpuIds.ElementAtOrDefault(PreferredGpuIndex);
config.Graphics.EnableShaderCache.Value = EnableShaderCache; config.Graphics.EnableShaderCache.Value = EnableShaderCache;
config.Graphics.EnableTextureRecompression.Value = EnableTextureRecompression; config.Graphics.EnableTextureRecompression.Value = EnableTextureRecompression;
config.Graphics.EnableMacroHLE.Value = EnableMacroHLE; config.Graphics.EnableMacroHLE.Value = EnableMacroHLE;
config.Graphics.GraphicsBackend.Value = (GraphicsBackend)GraphicsBackendIndex; config.Graphics.ResScale.Value = ResolutionScale == 4 ? -1 : ResolutionScale + 1;
config.System.EnablePtc.Value = EnablePptc; config.Graphics.ResScaleCustom.Value = CustomResolutionScale;
config.System.EnableInternetAccess.Value = EnableInternetAccess; config.Graphics.MaxAnisotropy.Value = MaxAnisotropy == 0 ? -1 : MathF.Pow(2, MaxAnisotropy);
config.System.EnableFsIntegrityChecks.Value = EnableFsIntegrityChecks; config.Graphics.AspectRatio.Value = (AspectRatio)AspectRatio;
config.System.IgnoreMissingServices.Value = IgnoreMissingServices;
config.System.ExpandRam.Value = ExpandDramSize;
config.Hid.EnableKeyboard.Value = EnableKeyboard;
config.Hid.EnableMouse.Value = EnableMouse;
config.Ui.CustomThemePath.Value = CustomThemePath;
config.Ui.EnableCustomTheme.Value = EnableCustomTheme;
config.Ui.BaseStyle.Value = BaseStyleIndex == 0 ? "Light" : "Dark";
config.System.Language.Value = (Language)Language;
config.System.Region.Value = (Region)Region;
config.Graphics.PreferredGpu.Value = _gpuIds.ElementAtOrDefault(PreferredGpuIndex);
if (ConfigurationState.Instance.Graphics.BackendThreading != (BackendThreading)GraphicsBackendMultithreadingIndex) if (ConfigurationState.Instance.Graphics.BackendThreading != (BackendThreading)GraphicsBackendMultithreadingIndex)
{ {
@@ -434,22 +450,9 @@ namespace Ryujinx.Ava.UI.ViewModels
} }
config.Graphics.BackendThreading.Value = (BackendThreading)GraphicsBackendMultithreadingIndex; config.Graphics.BackendThreading.Value = (BackendThreading)GraphicsBackendMultithreadingIndex;
TimeSpan systemTimeOffset = DateOffset - DateTime.Now;
config.System.SystemTimeOffset.Value = systemTimeOffset.Seconds;
config.Graphics.ShadersDumpPath.Value = ShaderDumpPath; config.Graphics.ShadersDumpPath.Value = ShaderDumpPath;
config.System.FsGlobalAccessLogMode.Value = FsGlobalAccessLogMode;
config.System.MemoryManagerMode.Value = (MemoryManagerMode)MemoryMode;
float anisotropy = MaxAnisotropy == 0 ? -1 : MathF.Pow(2, MaxAnisotropy);
config.Graphics.MaxAnisotropy.Value = anisotropy;
config.Graphics.AspectRatio.Value = (AspectRatio)AspectRatio;
config.Graphics.ResScale.Value = ResolutionScale == 0 ? -1 : ResolutionScale;
config.Graphics.ResScaleCustom.Value = CustomResolutionScale;
config.System.AudioVolume.Value = Volume / 100;
// Audio
AudioBackend audioBackend = (AudioBackend)AudioBackend; AudioBackend audioBackend = (AudioBackend)AudioBackend;
if (audioBackend != config.System.AudioBackend.Value) if (audioBackend != config.System.AudioBackend.Value)
{ {
@@ -458,7 +461,23 @@ namespace Ryujinx.Ava.UI.ViewModels
Logger.Info?.Print(LogClass.Application, $"AudioBackend toggled to: {audioBackend}"); Logger.Info?.Print(LogClass.Application, $"AudioBackend toggled to: {audioBackend}");
} }
config.Hid.Hotkeys.Value = KeyboardHotkeys; config.System.AudioVolume.Value = Volume / 100;
// Network
config.System.EnableInternetAccess.Value = EnableInternetAccess;
// Logging
config.Logger.EnableFileLog.Value = EnableFileLog;
config.Logger.EnableStub.Value = EnableStub;
config.Logger.EnableInfo.Value = EnableInfo;
config.Logger.EnableWarn.Value = EnableWarn;
config.Logger.EnableError.Value = EnableError;
config.Logger.EnableTrace.Value = EnableTrace;
config.Logger.EnableGuest.Value = EnableGuest;
config.Logger.EnableDebug.Value = EnableDebug;
config.Logger.EnableFsAccessLog.Value = EnableFsAccessLog;
config.System.FsGlobalAccessLogMode.Value = FsGlobalAccessLogMode;
config.Logger.GraphicsDebugLevel.Value = (GraphicsDebugLevel)OpenglDebugLevel;
config.ToFileFormat().SaveConfig(Program.ConfigurationPath); config.ToFileFormat().SaveConfig(Program.ConfigurationPath);

View File

@@ -0,0 +1,226 @@
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Threading;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.FsSystem;
using LibHac.Ns;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using SpanHelpers = LibHac.Common.SpanHelpers;
using Path = System.IO.Path;
namespace Ryujinx.Ava.UI.ViewModels;
public class TitleUpdateViewModel : BaseModel
{
public TitleUpdateMetadata _titleUpdateWindowData;
public readonly string _titleUpdateJsonPath;
private VirtualFileSystem _virtualFileSystem { get; }
private ulong _titleId { get; }
private string _titleName { get; }
private AvaloniaList<TitleUpdateModel> _titleUpdates = new();
private AvaloniaList<object> _views = new();
private object _selectedUpdate;
public AvaloniaList<TitleUpdateModel> TitleUpdates
{
get => _titleUpdates;
set
{
_titleUpdates = value;
OnPropertyChanged();
}
}
public AvaloniaList<object> Views
{
get => _views;
set
{
_views = value;
OnPropertyChanged();
}
}
public object SelectedUpdate
{
get => _selectedUpdate;
set
{
_selectedUpdate = value;
OnPropertyChanged();
}
}
public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
{
_virtualFileSystem = virtualFileSystem;
_titleId = titleId;
_titleName = titleName;
_titleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "updates.json");
try
{
_titleUpdateWindowData = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(_titleUpdateJsonPath);
}
catch
{
Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {_titleId} at {_titleUpdateJsonPath}");
_titleUpdateWindowData = new TitleUpdateMetadata
{
Selected = "",
Paths = new List<string>()
};
}
LoadUpdates();
}
private void LoadUpdates()
{
foreach (string path in _titleUpdateWindowData.Paths)
{
AddUpdate(path);
}
TitleUpdateModel selected = TitleUpdates.FirstOrDefault(x => x.Path == _titleUpdateWindowData.Selected, null);
SelectedUpdate = selected;
SortUpdates();
}
public void SortUpdates()
{
var list = TitleUpdates.ToList();
list.Sort((first, second) =>
{
if (string.IsNullOrEmpty(first.Control.DisplayVersionString.ToString()))
{
return -1;
}
else if (string.IsNullOrEmpty(second.Control.DisplayVersionString.ToString()))
{
return 1;
}
return Version.Parse(first.Control.DisplayVersionString.ToString()).CompareTo(Version.Parse(second.Control.DisplayVersionString.ToString())) * -1;
});
Views.Clear();
Views.Add(new BaseModel());
Views.AddRange(list);
if (SelectedUpdate == null)
{
SelectedUpdate = Views[0];
}
else if (!TitleUpdates.Contains(SelectedUpdate))
{
if (Views.Count > 1)
{
SelectedUpdate = Views[1];
}
else
{
SelectedUpdate = Views[0];
}
}
}
private void AddUpdate(string path)
{
if (File.Exists(path) && TitleUpdates.All(x => x.Path != path))
{
using FileStream file = new(path, FileMode.Open, FileAccess.Read);
try
{
(Nca patchNca, Nca controlNca) = ApplicationLoader.GetGameUpdateDataFromPartition(_virtualFileSystem, new PartitionFileSystem(file.AsStorage()), _titleId.ToString("x16"), 0);
if (controlNca != null && patchNca != null)
{
ApplicationControlProperty controlData = new();
using UniqueRef<IFile> nacpFile = new();
controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref(), "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure();
TitleUpdates.Add(new TitleUpdateModel(controlData, path));
}
else
{
Dispatcher.UIThread.Post(async () =>
{
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]);
});
}
}
catch (Exception ex)
{
Dispatcher.UIThread.Post(async () =>
{
await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[LocaleKeys.DialogDlcLoadNcaErrorMessage], ex.Message, path));
});
}
}
}
public void RemoveUpdate(TitleUpdateModel update)
{
TitleUpdates.Remove(update);
SortUpdates();
}
public async void Add()
{
OpenFileDialog dialog = new()
{
Title = LocaleManager.Instance[LocaleKeys.SelectUpdateDialogTitle],
AllowMultiple = true
};
dialog.Filters.Add(new FileDialogFilter
{
Name = "NSP",
Extensions = { "nsp" }
});
if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
string[] files = await dialog.ShowAsync(desktop.MainWindow);
if (files != null)
{
foreach (string file in files)
{
AddUpdate(file);
}
}
}
SortUpdates();
}
}

View File

@@ -0,0 +1,230 @@
using Avalonia.Media;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.FsSystem;
using LibHac.Ncm;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.UI.Models;
using Ryujinx.HLE.FileSystem;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using Color = Avalonia.Media.Color;
namespace Ryujinx.Ava.UI.ViewModels
{
internal class UserFirmwareAvatarSelectorViewModel : BaseModel
{
private static readonly Dictionary<string, byte[]> _avatarStore = new();
private ObservableCollection<ProfileImageModel> _images;
private Color _backgroundColor = Colors.White;
private int _selectedIndex;
private byte[] _selectedImage;
public UserFirmwareAvatarSelectorViewModel()
{
_images = new ObservableCollection<ProfileImageModel>();
LoadImagesFromStore();
}
public Color BackgroundColor
{
get => _backgroundColor;
set
{
_backgroundColor = value;
OnPropertyChanged();
ChangeImageBackground();
}
}
public ObservableCollection<ProfileImageModel> Images
{
get => _images;
set
{
_images = value;
OnPropertyChanged();
}
}
public int SelectedIndex
{
get => _selectedIndex;
set
{
_selectedIndex = value;
if (_selectedIndex == -1)
{
SelectedImage = null;
}
else
{
SelectedImage = _images[_selectedIndex].Data;
}
OnPropertyChanged();
}
}
public byte[] SelectedImage
{
get => _selectedImage;
private set => _selectedImage = value;
}
private void LoadImagesFromStore()
{
Images.Clear();
foreach (var image in _avatarStore)
{
Images.Add(new ProfileImageModel(image.Key, image.Value));
}
}
private void ChangeImageBackground()
{
foreach (var image in Images)
{
image.BackgroundColor = new SolidColorBrush(BackgroundColor);
}
}
public static void PreloadAvatars(ContentManager contentManager, VirtualFileSystem virtualFileSystem)
{
if (_avatarStore.Count > 0)
{
return;
}
string contentPath = contentManager.GetInstalledContentPath(0x010000000000080A, StorageId.BuiltInSystem, NcaContentType.Data);
string avatarPath = virtualFileSystem.SwitchPathToSystemPath(contentPath);
if (!string.IsNullOrWhiteSpace(avatarPath))
{
using (IStorage ncaFileStream = new LocalStorage(avatarPath, FileAccess.Read, FileMode.Open))
{
Nca nca = new(virtualFileSystem.KeySet, ncaFileStream);
IFileSystem romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
foreach (DirectoryEntryEx item in romfs.EnumerateEntries())
{
// TODO: Parse DatabaseInfo.bin and table.bin files for more accuracy.
if (item.Type == DirectoryEntryType.File && item.FullPath.Contains("chara") && item.FullPath.Contains("szs"))
{
using var file = new UniqueRef<IFile>();
romfs.OpenFile(ref file.Ref(), ("/" + item.FullPath).ToU8Span(), OpenMode.Read).ThrowIfFailure();
using (MemoryStream stream = new())
using (MemoryStream streamPng = new())
{
file.Get.AsStream().CopyTo(stream);
stream.Position = 0;
Image avatarImage = Image.LoadPixelData<Rgba32>(DecompressYaz0(stream), 256, 256);
avatarImage.SaveAsPng(streamPng);
_avatarStore.Add(item.FullPath, streamPng.ToArray());
}
}
}
}
}
}
private static byte[] DecompressYaz0(Stream stream)
{
using (BinaryReader reader = new(stream))
{
reader.ReadInt32(); // Magic
uint decodedLength = BinaryPrimitives.ReverseEndianness(reader.ReadUInt32());
reader.ReadInt64(); // Padding
byte[] input = new byte[stream.Length - stream.Position];
stream.Read(input, 0, input.Length);
uint inputOffset = 0;
byte[] output = new byte[decodedLength];
uint outputOffset = 0;
ushort mask = 0;
byte header = 0;
while (outputOffset < decodedLength)
{
if ((mask >>= 1) == 0)
{
header = input[inputOffset++];
mask = 0x80;
}
if ((header & mask) != 0)
{
if (outputOffset == output.Length)
{
break;
}
output[outputOffset++] = input[inputOffset++];
}
else
{
byte byte1 = input[inputOffset++];
byte byte2 = input[inputOffset++];
uint dist = (uint)((byte1 & 0xF) << 8) | byte2;
uint position = outputOffset - (dist + 1);
uint length = (uint)byte1 >> 4;
if (length == 0)
{
length = (uint)input[inputOffset++] + 0x12;
}
else
{
length += 2;
}
uint gap = outputOffset - position;
uint nonOverlappingLength = length;
if (nonOverlappingLength > gap)
{
nonOverlappingLength = gap;
}
Buffer.BlockCopy(output, (int)position, output, (int)outputOffset, (int)nonOverlappingLength);
outputOffset += nonOverlappingLength;
position += nonOverlappingLength;
length -= nonOverlappingLength;
while (length-- > 0)
{
output[outputOffset++] = output[position++];
}
}
}
return output;
}
}
}
}

View File

@@ -0,0 +1,18 @@
namespace Ryujinx.Ava.UI.ViewModels
{
internal class UserProfileImageSelectorViewModel : BaseModel
{
private bool _firmwareFound;
public bool FirmwareFound
{
get => _firmwareFound;
set
{
_firmwareFound = value;
OnPropertyChanged();
}
}
}
}

View File

@@ -1,215 +1,25 @@
using Avalonia; using Microsoft.IdentityModel.Tokens;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Shim;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq;
using UserId = Ryujinx.HLE.HOS.Services.Account.Acc.UserId;
using UserProfile = Ryujinx.Ava.UI.Models.UserProfile; using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
namespace Ryujinx.Ava.UI.ViewModels namespace Ryujinx.Ava.UI.ViewModels
{ {
public class UserProfileViewModel : BaseModel, IDisposable public class UserProfileViewModel : BaseModel, IDisposable
{ {
private readonly NavigationDialogHost _owner;
private UserProfile _selectedProfile;
private UserProfile _highlightedProfile;
public UserProfileViewModel() public UserProfileViewModel()
{ {
Profiles = new ObservableCollection<UserProfile>(); Profiles = new ObservableCollection<BaseModel>();
LostProfiles = new ObservableCollection<UserProfile>(); LostProfiles = new ObservableCollection<UserProfile>();
IsEmpty = LostProfiles.IsNullOrEmpty();
} }
public UserProfileViewModel(NavigationDialogHost owner) : this() public ObservableCollection<BaseModel> Profiles { get; set; }
{
_owner = owner;
LoadProfiles();
}
public ObservableCollection<UserProfile> Profiles { get; set; }
public ObservableCollection<UserProfile> LostProfiles { get; set; } public ObservableCollection<UserProfile> LostProfiles { get; set; }
public UserProfile SelectedProfile public bool IsEmpty { get; set; }
{
get => _selectedProfile;
set
{
_selectedProfile = value;
OnPropertyChanged();
OnPropertyChanged(nameof(IsHighlightedProfileDeletable));
OnPropertyChanged(nameof(IsHighlightedProfileEditable));
}
}
public bool IsHighlightedProfileEditable => _highlightedProfile != null;
public bool IsHighlightedProfileDeletable => _highlightedProfile != null && _highlightedProfile.UserId != AccountManager.DefaultUserId;
public UserProfile HighlightedProfile
{
get => _highlightedProfile;
set
{
_highlightedProfile = value;
OnPropertyChanged();
OnPropertyChanged(nameof(IsHighlightedProfileDeletable));
OnPropertyChanged(nameof(IsHighlightedProfileEditable));
}
}
public void Dispose() { } public void Dispose() { }
public void LoadProfiles()
{
Profiles.Clear();
LostProfiles.Clear();
var profiles = _owner.AccountManager.GetAllUsers().OrderByDescending(x => x.AccountState == AccountState.Open);
foreach (var profile in profiles)
{
Profiles.Add(new UserProfile(profile, _owner));
}
SelectedProfile = Profiles.FirstOrDefault(x => x.UserId == _owner.AccountManager.LastOpenedUser.UserId);
if (SelectedProfile == null)
{
SelectedProfile = Profiles.First();
if (SelectedProfile != null)
{
_owner.AccountManager.OpenUser(_selectedProfile.UserId);
}
}
var saveDataFilter = SaveDataFilter.Make(programId: default, saveType: SaveDataType.Account,
default, saveDataId: default, index: default);
using var saveDataIterator = new UniqueRef<SaveDataIterator>();
_owner.HorizonClient.Fs.OpenSaveDataIterator(ref saveDataIterator.Ref(), SaveDataSpaceId.User, in saveDataFilter).ThrowIfFailure();
Span<SaveDataInfo> saveDataInfo = stackalloc SaveDataInfo[10];
HashSet<UserId> lostAccounts = new HashSet<UserId>();
while (true)
{
saveDataIterator.Get.ReadSaveDataInfo(out long readCount, saveDataInfo).ThrowIfFailure();
if (readCount == 0)
{
break;
}
for (int i = 0; i < readCount; i++)
{
var save = saveDataInfo[i];
var id = new UserId((long)save.UserId.Id.Low, (long)save.UserId.Id.High);
if (Profiles.FirstOrDefault( x=> x.UserId == id) == null)
{
lostAccounts.Add(id);
}
}
}
foreach(var account in lostAccounts)
{
LostProfiles.Add(new UserProfile(new HLE.HOS.Services.Account.Acc.UserProfile(account, "", null), _owner));
}
}
public void AddUser()
{
UserProfile userProfile = null;
_owner.Navigate(typeof(UserEditor), (this._owner, userProfile, true));
}
public async void ManageSaves()
{
UserProfile userProfile = _highlightedProfile ?? SelectedProfile;
SaveManager manager = new SaveManager(userProfile, _owner.HorizonClient, _owner.VirtualFileSystem);
ContentDialog contentDialog = new ContentDialog
{
Title = string.Format(LocaleManager.Instance[LocaleKeys.SaveManagerHeading], userProfile.Name),
PrimaryButtonText = "",
SecondaryButtonText = "",
CloseButtonText = LocaleManager.Instance[LocaleKeys.UserProfilesClose],
Content = manager,
Padding = new Thickness(0)
};
await contentDialog.ShowAsync();
}
public void EditUser()
{
_owner.Navigate(typeof(UserEditor), (this._owner, _highlightedProfile ?? SelectedProfile, false));
}
public async void DeleteUser()
{
if (_highlightedProfile != null)
{
var lastUserId = _owner.AccountManager.LastOpenedUser.UserId;
if (_highlightedProfile.UserId == lastUserId)
{
// If we are deleting the currently open profile, then we must open something else before deleting.
var profile = Profiles.FirstOrDefault(x => x.UserId != lastUserId);
if (profile == null)
{
Dispatcher.UIThread.Post(async () =>
{
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUserProfileDeletionWarningMessage]);
});
return;
}
_owner.AccountManager.OpenUser(profile.UserId);
}
var result =
await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance[LocaleKeys.DialogUserProfileDeletionConfirmMessage], "",
LocaleManager.Instance[LocaleKeys.InputDialogYes], LocaleManager.Instance[LocaleKeys.InputDialogNo], "");
if (result == UserResult.Yes)
{
_owner.AccountManager.DeleteUser(_highlightedProfile.UserId);
}
}
LoadProfiles();
}
public void GoBack()
{
_owner.GoBack();
}
public void RecoverLostAccounts()
{
_owner.Navigate(typeof(UserRecoverer), (this._owner, this));
}
} }
} }

View File

@@ -0,0 +1,121 @@
using DynamicData;
using DynamicData.Binding;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Models;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace Ryujinx.Ava.UI.ViewModels
{
public class UserSaveManagerViewModel : BaseModel
{
private int _sortIndex;
private int _orderIndex;
private string _search;
private ObservableCollection<SaveModel> _saves = new();
private ObservableCollection<SaveModel> _views = new();
private AccountManager _accountManager;
public string SaveManagerHeading =>
string.Format(LocaleManager.Instance[LocaleKeys.SaveManagerHeading], _accountManager.LastOpenedUser.Name, _accountManager.LastOpenedUser.UserId);
public int SortIndex
{
get => _sortIndex;
set
{
_sortIndex = value;
OnPropertyChanged();
Sort();
}
}
public int OrderIndex
{
get => _orderIndex;
set
{
_orderIndex = value;
OnPropertyChanged();
Sort();
}
}
public string Search
{
get => _search;
set
{
_search = value;
OnPropertyChanged();
Sort();
}
}
public ObservableCollection<SaveModel> Saves
{
get => _saves;
set
{
_saves = value;
OnPropertyChanged();
Sort();
}
}
public ObservableCollection<SaveModel> Views
{
get => _views;
set
{
_views = value;
OnPropertyChanged();
}
}
public UserSaveManagerViewModel(AccountManager accountManager)
{
_accountManager = accountManager;
}
public void Sort()
{
Saves.AsObservableChangeSet()
.Filter(Filter)
.Sort(GetComparer())
.Bind(out var view).AsObservableList();
_views.Clear();
_views.AddRange(view);
OnPropertyChanged(nameof(Views));
}
private bool Filter(object arg)
{
if (arg is SaveModel save)
{
return string.IsNullOrWhiteSpace(_search) || save.Title.ToLower().Contains(_search.ToLower());
}
return false;
}
private IComparer<SaveModel> GetComparer()
{
switch (SortIndex)
{
case 0:
return OrderIndex == 0
? SortExpressionComparer<SaveModel>.Ascending(save => save.Title)
: SortExpressionComparer<SaveModel>.Descending(save => save.Title);
case 1:
return OrderIndex == 0
? SortExpressionComparer<SaveModel>.Ascending(save => save.Size)
: SortExpressionComparer<SaveModel>.Descending(save => save.Size);
default:
return null;
}
}
}
}

View File

@@ -74,7 +74,6 @@
Margin="5,0,5,0" Margin="5,0,5,0"
HorizontalAlignment="Right" HorizontalAlignment="Right"
VerticalAlignment="Center" VerticalAlignment="Center"
VerticalContentAlignment="Center"
DockPanel.Dock="Right" DockPanel.Dock="Right"
KeyUp="SearchBox_OnKeyUp" KeyUp="SearchBox_OnKeyUp"
Text="{Binding SearchText}" Text="{Binding SearchText}"

View File

@@ -82,9 +82,6 @@
Width="350" Width="350"
HorizontalContentAlignment="Left" HorizontalContentAlignment="Left"
ToolTip.Tip="{locale:Locale ResolutionScaleTooltip}"> ToolTip.Tip="{locale:Locale ResolutionScaleTooltip}">
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScaleCustom}" />
</ComboBoxItem>
<ComboBoxItem> <ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScaleNative}" /> <TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScaleNative}" />
</ComboBoxItem> </ComboBoxItem>
@@ -97,6 +94,9 @@
<ComboBoxItem> <ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScale4x}" /> <TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScale4x}" />
</ComboBoxItem> </ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScaleCustom}" />
</ComboBoxItem>
</ComboBox> </ComboBox>
<ui:NumberBox <ui:NumberBox
Margin="10,0,0,0" Margin="10,0,0,0"

View File

@@ -47,31 +47,34 @@
ToolTip.Tip="{locale:Locale ErrorLogTooltip}"> ToolTip.Tip="{locale:Locale ErrorLogTooltip}">
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableErrorLogs}" /> <TextBlock Text="{locale:Locale SettingsTabLoggingEnableErrorLogs}" />
</CheckBox> </CheckBox>
<CheckBox IsChecked="{Binding EnableTrace}"
ToolTip.Tip="{locale:Locale TraceLogTooltip}">
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableTraceLogs}" />
</CheckBox>
<CheckBox IsChecked="{Binding EnableGuest}" <CheckBox IsChecked="{Binding EnableGuest}"
ToolTip.Tip="{locale:Locale GuestLogTooltip}"> ToolTip.Tip="{locale:Locale GuestLogTooltip}">
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableGuestLogs}" /> <TextBlock Text="{locale:Locale SettingsTabLoggingEnableGuestLogs}" />
</CheckBox> </CheckBox>
</StackPanel> </StackPanel>
<Separator Height="1" /> <Separator Height="1" />
<StackPanel Orientation="Vertical" Spacing="2">
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabLoggingDeveloperOptions}" /> <TextBlock Classes="h1" Text="{locale:Locale SettingsTabLoggingDeveloperOptions}" />
<TextBlock Foreground="{DynamicResource SecondaryTextColor}" Text="{locale:Locale SettingsTabLoggingDeveloperOptionsNote}" />
</StackPanel>
<StackPanel <StackPanel
Margin="10,0,0,0" Margin="10,0,0,0"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Orientation="Vertical" Orientation="Vertical"
Spacing="10"> Spacing="10">
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical">
<CheckBox IsChecked="{Binding EnableDebug}" <CheckBox IsChecked="{Binding EnableTrace}"
ToolTip.Tip="{locale:Locale DebugLogTooltip}"> ToolTip.Tip="{locale:Locale TraceLogTooltip}">
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableDebugLogs}" /> <TextBlock Text="{locale:Locale SettingsTabLoggingEnableTraceLogs}" />
</CheckBox> </CheckBox>
<CheckBox IsChecked="{Binding EnableFsAccessLog}" <CheckBox IsChecked="{Binding EnableFsAccessLog}"
ToolTip.Tip="{locale:Locale FileAccessLogTooltip}"> ToolTip.Tip="{locale:Locale FileAccessLogTooltip}">
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableFsAccessLogs}" /> <TextBlock Text="{locale:Locale SettingsTabLoggingEnableFsAccessLogs}" />
</CheckBox> </CheckBox>
<CheckBox IsChecked="{Binding EnableDebug}"
ToolTip.Tip="{locale:Locale DebugLogTooltip}">
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableDebugLogs}" />
</CheckBox>
<StackPanel Margin="0,10,0,0" Orientation="Horizontal" VerticalAlignment="Stretch"> <StackPanel Margin="0,10,0,0" Orientation="Horizontal" VerticalAlignment="Stretch">
<TextBlock VerticalAlignment="Center" <TextBlock VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale FSAccessLogModeTooltip}" ToolTip.Tip="{locale:Locale FSAccessLogModeTooltip}"

View File

@@ -172,9 +172,9 @@
</CheckBox> </CheckBox>
</StackPanel> </StackPanel>
<Separator Height="1" /> <Separator Height="1" />
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Vertical" Spacing="2">
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabSystemHacks}" /> <TextBlock Classes="h1" Text="{locale:Locale SettingsTabSystemHacks}" />
<TextBlock Text="{locale:Locale SettingsTabSystemHacksNote}" /> <TextBlock Foreground="{DynamicResource SecondaryTextColor}" Text="{locale:Locale SettingsTabSystemHacksNote}" />
</StackPanel> </StackPanel>
<StackPanel <StackPanel
Margin="10,0,0,0" Margin="10,0,0,0"

View File

@@ -1,16 +1,20 @@
<UserControl <UserControl
x:Class="Ryujinx.Ava.UI.Controls.UserEditor" x:Class="Ryujinx.Ava.UI.Views.User.UserEditorView"
xmlns="https://github.com/avaloniaui" xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
Margin="0" Margin="0"
MinWidth="500" MinWidth="500"
Padding="0" Padding="0"
mc:Ignorable="d" mc:Ignorable="d"
Focusable="True"> Focusable="True"
x:CompileBindings="True"
x:DataType="models:TempProfile">
<UserControl.Resources> <UserControl.Resources>
<helpers:BitmapArrayValueConverter x:Key="ByteImage" /> <helpers:BitmapArrayValueConverter x:Key="ByteImage" />
</UserControl.Resources> </UserControl.Resources>
@@ -23,35 +27,9 @@
<RowDefinition Height="*" /> <RowDefinition Height="*" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<StackPanel
HorizontalAlignment="Left"
VerticalAlignment="Stretch"
Orientation="Vertical">
<Image
Name="ProfileImage"
Width="96"
Height="96"
Margin="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Source="{Binding Image, Converter={StaticResource ByteImage}}" />
<Button
Name="ChangePictureButton"
Margin="5"
HorizontalAlignment="Stretch"
Click="ChangePictureButton_Click"
Content="{locale:Locale UserProfilesChangeProfileImage}" />
<Button
Name="AddPictureButton"
Margin="5"
HorizontalAlignment="Stretch"
Click="ChangePictureButton_Click"
Content="{locale:Locale UserProfilesSetProfileImage}" />
</StackPanel>
<StackPanel <StackPanel
Grid.Row="0" Grid.Row="0"
Grid.Column="1" Grid.Column="0"
Margin="5,10"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Orientation="Vertical" Orientation="Vertical"
Spacing="10"> Spacing="10">
@@ -61,9 +39,60 @@
Width="300" Width="300"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
MaxLength="{Binding MaxProfileNameLength}" MaxLength="{Binding MaxProfileNameLength}"
Watermark="{locale:Locale ProfileNameSelectionWatermark}"
Text="{Binding Name}" /> Text="{Binding Name}" />
<TextBlock Name="IdText" Text="{locale:Locale UserProfilesUserId}" /> <TextBlock Name="IdText" Text="{locale:Locale UserProfilesUserId}" />
<TextBlock Name="IdLabel" Text="{Binding UserId}" /> <TextBox
Name="IdLabel"
Width="300"
HorizontalAlignment="Stretch"
IsReadOnly="True"
Text="{Binding UserIdString}" />
</StackPanel>
<StackPanel
Grid.Row="0"
Grid.Column="1"
HorizontalAlignment="Right"
VerticalAlignment="Stretch"
Orientation="Vertical">
<Border
BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
BorderThickness="1">
<Panel>
<ui:SymbolIcon
FontSize="60"
Width="96"
Height="96"
Margin="0"
Foreground="{DynamicResource AppListHoverBackgroundColor}"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Symbol="Camera" />
<Image
Name="ProfileImage"
Width="96"
Height="96"
Margin="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Source="{Binding Image, Converter={StaticResource ByteImage}}" />
</Panel>
</Border>
</StackPanel>
<StackPanel
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
HorizontalAlignment="Left"
Orientation="Horizontal"
Margin="0 24 0 0"
Spacing="10">
<Button
Width="50"
MinWidth="50"
Click="BackButton_Click">
<ui:SymbolIcon Symbol="Back" />
</Button>
</StackPanel> </StackPanel>
<StackPanel <StackPanel
Grid.Row="1" Grid.Row="1"
@@ -71,16 +100,24 @@
Grid.ColumnSpan="2" Grid.ColumnSpan="2"
HorizontalAlignment="Right" HorizontalAlignment="Right"
Orientation="Horizontal" Orientation="Horizontal"
Margin="0 24 0 0"
Spacing="10"> Spacing="10">
<Button
Name="DeleteButton"
Click="DeleteButton_Click"
Content="{locale:Locale UserProfilesDelete}" />
<Button
Name="ChangePictureButton"
Click="ChangePictureButton_Click"
Content="{locale:Locale UserProfilesChangeProfileImage}" />
<Button
Name="AddPictureButton"
Click="ChangePictureButton_Click"
Content="{locale:Locale UserProfilesSetProfileImage}" />
<Button <Button
Name="SaveButton" Name="SaveButton"
Click="SaveButton_Click" Click="SaveButton_Click"
Content="{locale:Locale Save}" /> Content="{locale:Locale Save}" />
<Button
Name="CloseButton"
HorizontalAlignment="Right"
Click="CloseButton_Click"
Content="{locale:Locale Discard}" />
</StackPanel> </StackPanel>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@@ -4,13 +4,16 @@ using Avalonia.Interactivity;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Navigation; using FluentAvalonia.UI.Navigation;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using System;
using UserProfile = Ryujinx.Ava.UI.Models.UserProfile; using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
namespace Ryujinx.Ava.UI.Controls namespace Ryujinx.Ava.UI.Views.User
{ {
public partial class UserEditor : UserControl public partial class UserEditorView : UserControl
{ {
private NavigationDialogHost _parent; private NavigationDialogHost _parent;
private UserProfile _profile; private UserProfile _profile;
@@ -18,8 +21,9 @@ namespace Ryujinx.Ava.UI.Controls
public TempProfile TempProfile { get; set; } public TempProfile TempProfile { get; set; }
public uint MaxProfileNameLength => 0x20; public uint MaxProfileNameLength => 0x20;
public bool IsDeletable => _profile.UserId != AccountManager.DefaultUserId;
public UserEditor() public UserEditorView()
{ {
InitializeComponent(); InitializeComponent();
AddHandler(Frame.NavigatedToEvent, (s, e) => AddHandler(Frame.NavigatedToEvent, (s, e) =>
@@ -44,41 +48,84 @@ namespace Ryujinx.Ava.UI.Controls
break; break;
} }
((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle]} - " +
$"{ (_isNewUser ? LocaleManager.Instance[LocaleKeys.UserEditorTitleCreate] : LocaleManager.Instance[LocaleKeys.UserEditorTitle])}";
DataContext = TempProfile; DataContext = TempProfile;
AddPictureButton.IsVisible = _isNewUser; AddPictureButton.IsVisible = _isNewUser;
ChangePictureButton.IsVisible = !_isNewUser;
IdLabel.IsVisible = _profile != null; IdLabel.IsVisible = _profile != null;
IdText.IsVisible = _profile != null; IdText.IsVisible = _profile != null;
ChangePictureButton.IsVisible = !_isNewUser; if (!_isNewUser && IsDeletable)
{
DeleteButton.IsVisible = true;
}
else
{
DeleteButton.IsVisible = false;
}
} }
} }
private void CloseButton_Click(object sender, RoutedEventArgs e) private async void BackButton_Click(object sender, RoutedEventArgs e)
{
if (_isNewUser)
{
if (TempProfile.Name != String.Empty || TempProfile.Image != null)
{
if (await ContentDialogHelper.CreateChoiceDialog(
LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesTitle],
LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesMessage],
LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesSubMessage]))
{ {
_parent?.GoBack(); _parent?.GoBack();
} }
}
else
{
_parent?.GoBack();
}
}
else
{
if (_profile.Name != TempProfile.Name || _profile.Image != TempProfile.Image)
{
if (await ContentDialogHelper.CreateChoiceDialog(
LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesTitle],
LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesMessage],
LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesSubMessage]))
{
_parent?.GoBack();
}
}
else
{
_parent?.GoBack();
}
}
}
private async void SaveButton_Click(object sender, RoutedEventArgs e) private void DeleteButton_Click(object sender, RoutedEventArgs e)
{
_parent.DeleteUser(_profile);
}
private void SaveButton_Click(object sender, RoutedEventArgs e)
{ {
DataValidationErrors.ClearErrors(NameBox); DataValidationErrors.ClearErrors(NameBox);
bool isInvalid = false;
if (string.IsNullOrWhiteSpace(TempProfile.Name)) if (string.IsNullOrWhiteSpace(TempProfile.Name))
{ {
DataValidationErrors.SetError(NameBox, new DataValidationException(LocaleManager.Instance[LocaleKeys.UserProfileEmptyNameError])); DataValidationErrors.SetError(NameBox, new DataValidationException(LocaleManager.Instance[LocaleKeys.UserProfileEmptyNameError]));
isInvalid = true; return;
} }
if (TempProfile.Image == null) if (TempProfile.Image == null)
{ {
await ContentDialogHelper.CreateWarningDialog(LocaleManager.Instance[LocaleKeys.UserProfileNoImageError], ""); _parent.Navigate(typeof(UserProfileImageSelectorView), (_parent, TempProfile));
isInvalid = true;
}
if(isInvalid)
{
return; return;
} }
@@ -104,7 +151,7 @@ namespace Ryujinx.Ava.UI.Controls
public void SelectProfileImage() public void SelectProfileImage()
{ {
_parent.Navigate(typeof(ProfileImageSelectionDialog), (_parent, TempProfile)); _parent.Navigate(typeof(UserProfileImageSelectorView), (_parent, TempProfile));
} }
private void ChangePictureButton_Click(object sender, RoutedEventArgs e) private void ChangePictureButton_Click(object sender, RoutedEventArgs e)

View File

@@ -0,0 +1,114 @@
<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:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Width="528"
d:DesignWidth="578"
d:DesignHeight="350"
x:Class="Ryujinx.Ava.UI.Views.User.UserFirmwareAvatarSelectorView"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
x:CompileBindings="True"
x:DataType="viewModels:UserFirmwareAvatarSelectorViewModel"
Focusable="True">
<Design.DataContext>
<viewModels:UserFirmwareAvatarSelectorViewModel />
</Design.DataContext>
<UserControl.Resources>
<helpers:BitmapArrayValueConverter x:Key="ByteImage" />
</UserControl.Resources>
<Grid
Margin="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox
Grid.Row="1"
BorderThickness="0"
SelectedIndex="{Binding SelectedIndex}"
Height="400"
Items="{Binding Images}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel
Orientation="Horizontal"
Margin="0"
HorizontalAlignment="Center" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="CornerRadius" Value="4" />
<Setter Property="Width" Value="85" />
<Setter Property="MaxWidth" Value="85" />
<Setter Property="MinWidth" Value="85" />
</Style>
<Style Selector="ListBoxItem /template/ Border#SelectionIndicator">
<Setter Property="MinHeight" Value="70" />
</Style>
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate>
<Panel
Background="{Binding BackgroundColor}"
Margin="5">
<Image Source="{Binding Data, Converter={StaticResource ByteImage}}" />
</Panel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel
Grid.Row="3"
Orientation="Horizontal"
Spacing="10"
Margin="0 24 0 0"
HorizontalAlignment="Left">
<Button
Width="50"
MinWidth="50"
Height="35"
Click="GoBack">
<ui:SymbolIcon Symbol="Back" />
</Button>
</StackPanel>
<StackPanel
Grid.Row="3"
Orientation="Horizontal"
Spacing="10"
Margin="0 24 0 0"
HorizontalAlignment="Right">
<ui:ColorPickerButton
FlyoutPlacement="Top"
IsMoreButtonVisible="False"
UseColorPalette="False"
UseColorTriangle="False"
UseColorWheel="False"
ShowAcceptDismissButtons="False"
IsAlphaEnabled="False"
Color="{Binding BackgroundColor, Mode=TwoWay}"
Name="ColorButton">
<ui:ColorPickerButton.Styles>
<Style Selector="Grid#Root > DockPanel > Grid">
<Setter Property="IsVisible" Value="False" />
</Style>
</ui:ColorPickerButton.Styles>
</ui:ColorPickerButton>
<Button
Content="{locale:Locale AvatarChoose}"
Height="35"
Name="ChooseButton"
Click="ChooseButton_OnClick" />
</StackPanel>
</Grid>
</UserControl>

View File

@@ -6,15 +6,20 @@ using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using System.IO;
namespace Ryujinx.Ava.UI.Windows namespace Ryujinx.Ava.UI.Views.User
{ {
public partial class AvatarWindow : UserControl public partial class UserFirmwareAvatarSelectorView : UserControl
{ {
private NavigationDialogHost _parent; private NavigationDialogHost _parent;
private TempProfile _profile; private TempProfile _profile;
public AvatarWindow(ContentManager contentManager) public UserFirmwareAvatarSelectorView(ContentManager contentManager)
{ {
ContentManager = contentManager; ContentManager = contentManager;
@@ -23,7 +28,7 @@ namespace Ryujinx.Ava.UI.Windows
InitializeComponent(); InitializeComponent();
} }
public AvatarWindow() public UserFirmwareAvatarSelectorView()
{ {
InitializeComponent(); InitializeComponent();
@@ -43,7 +48,7 @@ namespace Ryujinx.Ava.UI.Windows
ContentManager = _parent.ContentManager; ContentManager = _parent.ContentManager;
if (Program.PreviewerDetached) if (Program.PreviewerDetached)
{ {
ViewModel = new AvatarProfileViewModel(() => ViewModel.ReloadImages()); ViewModel = new UserFirmwareAvatarSelectorViewModel();
} }
DataContext = ViewModel; DataContext = ViewModel;
@@ -53,22 +58,28 @@ namespace Ryujinx.Ava.UI.Windows
public ContentManager ContentManager { get; private set; } public ContentManager ContentManager { get; private set; }
internal AvatarProfileViewModel ViewModel { get; set; } internal UserFirmwareAvatarSelectorViewModel ViewModel { get; set; }
private void CloseButton_OnClick(object sender, RoutedEventArgs e) private void GoBack(object sender, RoutedEventArgs e)
{ {
ViewModel.Dispose();
_parent.GoBack(); _parent.GoBack();
} }
private void ChooseButton_OnClick(object sender, RoutedEventArgs e) private void ChooseButton_OnClick(object sender, RoutedEventArgs e)
{ {
if (ViewModel.SelectedIndex > -1) if (ViewModel.SelectedImage != null)
{ {
_profile.Image = ViewModel.SelectedImage; MemoryStream streamJpg = new();
SixLabors.ImageSharp.Image avatarImage = SixLabors.ImageSharp.Image.Load(ViewModel.SelectedImage, new PngDecoder());
ViewModel.Dispose(); avatarImage.Mutate(x => x.BackgroundColor(new Rgba32(
ViewModel.BackgroundColor.R,
ViewModel.BackgroundColor.G,
ViewModel.BackgroundColor.B,
ViewModel.BackgroundColor.A)));
avatarImage.SaveAsJpeg(streamJpg);
_profile.Image = streamJpg.ToArray();
_parent.GoBack(); _parent.GoBack();
} }

View File

@@ -0,0 +1,63 @@
<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:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:viewModles="clr-namespace:Ryujinx.Ava.UI.ViewModels"
Focusable="True"
mc:Ignorable="d"
x:Class="Ryujinx.Ava.UI.Views.User.UserProfileImageSelectorView"
x:CompileBindings="True"
x:DataType="viewModles:UserProfileImageSelectorViewModel"
Width="500"
d:DesignWidth="500">
<Design.DataContext>
<viewModles:UserProfileImageSelectorViewModel />
</Design.DataContext>
<Grid
HorizontalAlignment="Stretch"
VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="70" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
TextWrapping="Wrap"
HorizontalAlignment="Left"
TextAlignment="Left"
Text="{locale:Locale ProfileImageSelectionNote}" />
<StackPanel
Grid.Row="2"
Spacing="10"
HorizontalAlignment="Left"
Orientation="Horizontal">
<Button
Width="50"
MinWidth="50"
Click="GoBack">
<ui:SymbolIcon Symbol="Back" />
</Button>
</StackPanel>
<StackPanel
Grid.Row="2"
Spacing="10"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
Name="Import"
Click="Import_OnClick">
<TextBlock Text="{locale:Locale ProfileImageSelectionImportImage}" />
</Button>
<Button
Name="SelectFirmwareImage"
IsEnabled="{Binding FirmwareFound}"
Click="SelectFirmwareImage_OnClick">
<TextBlock Text="{locale:Locale ProfileImageSelectionSelectAvatar}" />
</Button>
</StackPanel>
</Grid>
</UserControl>

View File

@@ -4,25 +4,26 @@ using Avalonia.VisualTree;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Navigation; using FluentAvalonia.UI.Navigation;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
using System.IO; using System.IO;
using Image = SixLabors.ImageSharp.Image; using Image = SixLabors.ImageSharp.Image;
namespace Ryujinx.Ava.UI.Controls namespace Ryujinx.Ava.UI.Views.User
{ {
public partial class ProfileImageSelectionDialog : UserControl public partial class UserProfileImageSelectorView : UserControl
{ {
private ContentManager _contentManager; private ContentManager _contentManager;
private NavigationDialogHost _parent; private NavigationDialogHost _parent;
private TempProfile _profile; private TempProfile _profile;
public bool FirmwareFound => _contentManager.GetCurrentFirmwareVersion() != null; internal UserProfileImageSelectorViewModel ViewModel { get; private set; }
public ProfileImageSelectionDialog() public UserProfileImageSelectorView()
{ {
InitializeComponent(); InitializeComponent();
AddHandler(Frame.NavigatedToEvent, (s, e) => AddHandler(Frame.NavigatedToEvent, (s, e) =>
@@ -40,13 +41,23 @@ namespace Ryujinx.Ava.UI.Controls
case NavigationMode.New: case NavigationMode.New:
(_parent, _profile) = ((NavigationDialogHost, TempProfile))arg.Parameter; (_parent, _profile) = ((NavigationDialogHost, TempProfile))arg.Parameter;
_contentManager = _parent.ContentManager; _contentManager = _parent.ContentManager;
break;
case NavigationMode.Back: ((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle]} - {LocaleManager.Instance[LocaleKeys.ProfileImageSelectionHeader]}";
_parent.GoBack();
break; if (Program.PreviewerDetached)
{
DataContext = ViewModel = new UserProfileImageSelectorViewModel();
ViewModel.FirmwareFound = _contentManager.GetCurrentFirmwareVersion() != null;
} }
DataContext = this; break;
case NavigationMode.Back:
if (_profile.Image != null)
{
_parent.GoBack();
}
break;
}
} }
} }
@@ -73,17 +84,25 @@ namespace Ryujinx.Ava.UI.Controls
string imageFile = image[0]; string imageFile = image[0];
_profile.Image = ProcessProfileImage(File.ReadAllBytes(imageFile)); _profile.Image = ProcessProfileImage(File.ReadAllBytes(imageFile));
}
if (_profile.Image != null)
{
_parent.GoBack(); _parent.GoBack();
} }
} }
}
}
private void GoBack(object sender, RoutedEventArgs e)
{
_parent.GoBack();
}
private void SelectFirmwareImage_OnClick(object sender, RoutedEventArgs e) private void SelectFirmwareImage_OnClick(object sender, RoutedEventArgs e)
{ {
if (FirmwareFound) if (ViewModel.FirmwareFound)
{ {
_parent.Navigate(typeof(AvatarWindow), (_parent, _profile)); _parent.Navigate(typeof(UserFirmwareAvatarSelectorView), (_parent, _profile));
} }
} }

View File

@@ -0,0 +1,83 @@
<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"
mc:Ignorable="d"
d:DesignWidth="550"
d:DesignHeight="450"
Width="500"
Height="400"
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"
x:Class="Ryujinx.Ava.UI.Views.User.UserRecovererView"
x:CompileBindings="True"
x:DataType="viewModels:UserProfileViewModel"
Focusable="True">
<Design.DataContext>
<viewModels:UserProfileViewModel />
</Design.DataContext>
<Grid HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Border
CornerRadius="5"
BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
BorderThickness="1"
Grid.Row="0">
<Panel>
<ListBox
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Items="{Binding LostProfiles}">
<ListBox.ItemTemplate>
<DataTemplate>
<Border
Margin="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ClipToBounds="True"
CornerRadius="5">
<Grid Margin="0">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock
HorizontalAlignment="Stretch"
Text="{Binding UserId}"
TextAlignment="Left"
TextWrapping="Wrap" />
<Button Grid.Column="1"
HorizontalAlignment="Right"
Click="Recover"
CommandParameter="{Binding}"
Content="{locale:Locale Recover}"/>
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBlock
IsVisible="{Binding IsEmpty}"
TextAlignment="Center"
Text="{locale:Locale UserProfilesRecoverEmptyList}"/>
</Panel>
</Border>
<StackPanel
Grid.Row="1"
Margin="0 24 0 0"
Orientation="Horizontal">
<Button
Width="50"
MinWidth="50"
Click="GoBack">
<ui:SymbolIcon Symbol="Back"/>
</Button>
</StackPanel>
</Grid>
</UserControl>

View File

@@ -0,0 +1,51 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Navigation;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
namespace Ryujinx.Ava.UI.Views.User
{
public partial class UserRecovererView : UserControl
{
private NavigationDialogHost _parent;
public UserRecovererView()
{
InitializeComponent();
AddHandler(Frame.NavigatedToEvent, (s, e) =>
{
NavigatedTo(e);
}, RoutingStrategies.Direct);
}
private void NavigatedTo(NavigationEventArgs arg)
{
if (Program.PreviewerDetached)
{
switch (arg.NavigationMode)
{
case NavigationMode.New:
var parent = (NavigationDialogHost)arg.Parameter;
_parent = parent;
((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle]} - {LocaleManager.Instance[LocaleKeys.UserProfilesRecoverHeading]}";
break;
}
}
}
private void GoBack(object sender, RoutedEventArgs e)
{
_parent?.GoBack();
}
private void Recover(object sender, RoutedEventArgs e)
{
_parent?.RecoverLostAccounts();
}
}
}

View File

@@ -0,0 +1,215 @@
<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:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
mc:Ignorable="d"
d:DesignWidth="600"
d:DesignHeight="500"
Height="450"
Width="550"
x:Class="Ryujinx.Ava.UI.Views.User.UserSaveManagerView"
x:CompileBindings="True"
x:DataType="viewModels:UserSaveManagerViewModel"
Focusable="True">
<Design.DataContext>
<viewModels:UserSaveManagerViewModel />
</Design.DataContext>
<UserControl.Resources>
<helpers:BitmapArrayValueConverter x:Key="ByteImage" />
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid
Grid.Row="0"
HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<StackPanel
Spacing="10"
Orientation="Horizontal"
HorizontalAlignment="Left"
VerticalAlignment="Center">
<Label Content="{locale:Locale CommonSort}" VerticalAlignment="Center" />
<ComboBox SelectedIndex="{Binding SortIndex}" Width="100">
<ComboBoxItem>
<Label
VerticalAlignment="Center"
HorizontalContentAlignment="Left"
Content="{locale:Locale Name}" />
</ComboBoxItem>
<ComboBoxItem>
<Label
VerticalAlignment="Center"
HorizontalContentAlignment="Left"
Content="{locale:Locale Size}" />
</ComboBoxItem>
<ComboBox.Styles>
<Style Selector="ContentControl#ContentPresenter">
<Setter Property="HorizontalAlignment" Value="Left" />
</Style>
</ComboBox.Styles>
</ComboBox>
<ComboBox SelectedIndex="{Binding OrderIndex}" Width="150">
<ComboBoxItem>
<Label
VerticalAlignment="Center"
HorizontalContentAlignment="Left"
Content="{locale:Locale OrderAscending}" />
</ComboBoxItem>
<ComboBoxItem>
<Label
VerticalAlignment="Center"
HorizontalContentAlignment="Left"
Content="{locale:Locale OrderDescending}" />
</ComboBoxItem>
<ComboBox.Styles>
<Style Selector="ContentControl#ContentPresenter">
<Setter Property="HorizontalAlignment" Value="Left" />
</Style>
</ComboBox.Styles>
</ComboBox>
</StackPanel>
<Grid
Grid.Column="1"
HorizontalAlignment="Stretch"
Margin="10,0, 0, 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Label Content="{locale:Locale Search}" VerticalAlignment="Center" />
<TextBox
Margin="5,0,0,0"
Grid.Column="1"
HorizontalAlignment="Stretch"
Text="{Binding Search}" />
</Grid>
</Grid>
<Border
Grid.Row="1"
Margin="0,5"
BorderThickness="1"
BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
CornerRadius="5"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<ListBox
Name="SaveList"
VirtualizationMode="None"
Items="{Binding Views}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Padding" Value="10" />
<Setter Property="Margin" Value="5" />
<Setter Property="CornerRadius" Value="4" />
</Style>
<Style Selector="ListBoxItem:selected /template/ Border#SelectionIndicator">
<Setter Property="IsVisible" Value="False" />
</Style>
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate x:DataType="models:SaveModel">
<Grid HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel
Grid.Column="0"
Orientation="Horizontal"
Spacing="5">
<Border
Height="42"
Width="42"
Padding="10"
BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
BorderThickness="1"
IsVisible="{Binding !InGameList}">
<ui:SymbolIcon
Symbol="Help"
FontSize="30"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<Image
IsVisible="{Binding InGameList}"
Width="42"
Height="42"
Source="{Binding Icon, Converter={StaticResource ByteImage}}" />
<TextBlock
MaxLines="3"
Width="320"
Margin="5"
TextWrapping="Wrap"
Text="{Binding Title}"
VerticalAlignment="Center" />
</StackPanel>
<StackPanel
Grid.Column="1"
Spacing="10"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Label
Content="{Binding SizeString}"
IsVisible="{Binding SizeAvailable}"
VerticalAlignment="Center"
HorizontalAlignment="Right" />
<Button
VerticalAlignment="Center"
HorizontalAlignment="Right"
Padding="10"
MinWidth="0"
MinHeight="0"
Name="OpenLocation"
Click="OpenLocation">
<ui:SymbolIcon
Symbol="OpenFolder"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Button>
<Button
VerticalAlignment="Center"
HorizontalAlignment="Right"
Padding="10"
MinWidth="0"
MinHeight="0"
Name="Delete"
Click="Delete">
<ui:SymbolIcon
Symbol="Delete"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Button>
</StackPanel>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
<StackPanel
Grid.Row="2"
Margin="0 24 0 0"
Orientation="Horizontal">
<Button
Width="50"
MinWidth="50"
Click="GoBack">
<ui:SymbolIcon Symbol="Back" />
</Button>
</StackPanel>
</Grid>
</UserControl>

View File

@@ -0,0 +1,147 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Navigation;
using LibHac;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Shim;
using Ryujinx.Ava.Common;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using UserId = LibHac.Fs.UserId;
namespace Ryujinx.Ava.UI.Views.User
{
public partial class UserSaveManagerView : UserControl
{
internal UserSaveManagerViewModel ViewModel { get; private set; }
private AccountManager _accountManager;
private HorizonClient _horizonClient;
private VirtualFileSystem _virtualFileSystem;
private NavigationDialogHost _parent;
public UserSaveManagerView()
{
InitializeComponent();
AddHandler(Frame.NavigatedToEvent, (s, e) =>
{
NavigatedTo(e);
}, RoutingStrategies.Direct);
}
private void NavigatedTo(NavigationEventArgs arg)
{
if (Program.PreviewerDetached)
{
switch (arg.NavigationMode)
{
case NavigationMode.New:
var args = ((NavigationDialogHost parent, AccountManager accountManager, HorizonClient client, VirtualFileSystem virtualFileSystem))arg.Parameter;
_accountManager = args.accountManager;
_horizonClient = args.client;
_virtualFileSystem = args.virtualFileSystem;
_parent = args.parent;
break;
}
DataContext = ViewModel = new UserSaveManagerViewModel(_accountManager);
((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle]} - {ViewModel.SaveManagerHeading}";
Task.Run(LoadSaves);
}
}
public void LoadSaves()
{
ViewModel.Saves.Clear();
var saves = new ObservableCollection<SaveModel>();
var saveDataFilter = SaveDataFilter.Make(
programId: default,
saveType: SaveDataType.Account,
new UserId((ulong)_accountManager.LastOpenedUser.UserId.High, (ulong)_accountManager.LastOpenedUser.UserId.Low),
saveDataId: default,
index: default);
using var saveDataIterator = new UniqueRef<SaveDataIterator>();
_horizonClient.Fs.OpenSaveDataIterator(ref saveDataIterator.Ref(), SaveDataSpaceId.User, in saveDataFilter).ThrowIfFailure();
Span<SaveDataInfo> saveDataInfo = stackalloc SaveDataInfo[10];
while (true)
{
saveDataIterator.Get.ReadSaveDataInfo(out long readCount, saveDataInfo).ThrowIfFailure();
if (readCount == 0)
{
break;
}
for (int i = 0; i < readCount; i++)
{
var save = saveDataInfo[i];
if (save.ProgramId.Value != 0)
{
var saveModel = new SaveModel(save, _virtualFileSystem);
saves.Add(saveModel);
}
}
}
Dispatcher.UIThread.Post(() =>
{
ViewModel.Saves = saves;
ViewModel.Sort();
});
}
private void GoBack(object sender, RoutedEventArgs e)
{
_parent?.GoBack();
}
private void OpenLocation(object sender, RoutedEventArgs e)
{
if (sender is Avalonia.Controls.Button button)
{
if (button.DataContext is SaveModel saveModel)
{
ApplicationHelper.OpenSaveDir(saveModel.SaveId);
}
}
}
private async void Delete(object sender, RoutedEventArgs e)
{
if (sender is Avalonia.Controls.Button button)
{
if (button.DataContext is SaveModel saveModel)
{
var result = await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance[LocaleKeys.DeleteUserSave],
LocaleManager.Instance[LocaleKeys.IrreversibleActionNote],
LocaleManager.Instance[LocaleKeys.InputDialogYes],
LocaleManager.Instance[LocaleKeys.InputDialogNo], "");
if (result == UserResult.Yes)
{
_horizonClient.Fs.DeleteSaveData(SaveDataSpaceId.User, saveModel.SaveId);
ViewModel.Saves.Remove(saveModel);
ViewModel.Sort();
}
}
}
}
}
}

View File

@@ -0,0 +1,165 @@
<UserControl
x:Class="Ryujinx.Ava.UI.Views.User.UserSelectorViews"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:flex="clr-namespace:Avalonia.Flexbox;assembly=Avalonia.Flexbox"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
d:DesignHeight="450"
MinWidth="500"
d:DesignWidth="800"
mc:Ignorable="d"
Focusable="True"
x:CompileBindings="True"
x:DataType="viewModels:UserProfileViewModel">
<UserControl.Resources>
<helpers:BitmapArrayValueConverter x:Key="ByteImage" />
</UserControl.Resources>
<Design.DataContext>
<viewModels:UserProfileViewModel />
</Design.DataContext>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border
CornerRadius="5"
BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
BorderThickness="1">
<ListBox
MaxHeight="300"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
SelectionChanged="ProfilesList_SelectionChanged"
Background="Transparent"
Items="{Binding Profiles}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<flex:FlexPanel
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
AlignContent="FlexStart"
JustifyContent="FlexStart" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Margin" Value="5 5 0 5" />
<Setter Property="CornerRadius" Value="5" />
</Style>
<Style Selector="Border#SelectionIndicator">
<Setter Property="Opacity" Value="0" />
</Style>
</ListBox.Styles>
<ListBox.DataTemplates>
<DataTemplate
DataType="models:UserProfile">
<Grid
PointerEnter="Grid_PointerEntered"
PointerLeave="Grid_OnPointerExited">
<Border
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ClipToBounds="True"
CornerRadius="5"
Background="{Binding BackgroundColor}">
<StackPanel
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Image
Width="96"
Height="96"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Source="{Binding Image, Converter={StaticResource ByteImage}}" />
<TextBlock
HorizontalAlignment="Stretch"
MaxWidth="90"
Text="{Binding Name}"
TextAlignment="Center"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis"
MaxLines="2"
Margin="5" />
</StackPanel>
</Border>
<Border
Margin="2"
Height="24"
Width="24"
CornerRadius="12"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Background="{DynamicResource ThemeContentBackgroundColor}"
IsVisible="{Binding IsPointerOver}">
<Button
MaxHeight="24"
MaxWidth="24"
MinHeight="24"
MinWidth="24"
CornerRadius="12"
Padding="0"
Click="EditUser">
<ui:SymbolIcon Symbol="Edit" />
</Button>
</Border>
</Grid>
</DataTemplate>
<DataTemplate
DataType="viewModels:BaseModel">
<Panel
Height="118"
Width="96">
<Button
MinWidth="50"
MinHeight="50"
MaxWidth="50"
MaxHeight="50"
CornerRadius="25"
Margin="10"
Padding="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Click="AddUser">
<ui:SymbolIcon Symbol="Add" />
</Button>
<Panel.Styles>
<Style Selector="Panel">
<Setter Property="Background" Value="{DynamicResource ListBoxBackground}"/>
</Style>
</Panel.Styles>
</Panel>
</DataTemplate>
</ListBox.DataTemplates>
</ListBox>
</Border>
<StackPanel
Grid.Row="1"
Margin="0 24 0 0"
HorizontalAlignment="Left"
Orientation="Horizontal"
Spacing="10">
<Button
Click="ManageSaves"
Content="{locale:Locale UserProfilesManageSaves}" />
<Button
Click="RecoverLostAccounts"
Content="{locale:Locale UserProfilesRecoverLostAccounts}" />
</StackPanel>
<StackPanel
Grid.Row="1"
Margin="0 24 0 0"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
Click="Close"
Content="{locale:Locale UserProfilesClose}" />
</StackPanel>
</Grid>
</UserControl>

View File

@@ -0,0 +1,128 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Navigation;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.ViewModels;
using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
namespace Ryujinx.Ava.UI.Views.User
{
public partial class UserSelectorViews : UserControl
{
private NavigationDialogHost _parent;
public UserProfileViewModel ViewModel { get; set; }
public UserSelectorViews()
{
InitializeComponent();
if (Program.PreviewerDetached)
{
AddHandler(Frame.NavigatedToEvent, (s, e) =>
{
NavigatedTo(e);
}, RoutingStrategies.Direct);
}
}
private void NavigatedTo(NavigationEventArgs arg)
{
if (Program.PreviewerDetached)
{
if (arg.NavigationMode == NavigationMode.New)
{
_parent = (NavigationDialogHost)arg.Parameter;
ViewModel = _parent.ViewModel;
}
if (arg.NavigationMode == NavigationMode.Back)
{
((ContentDialog)_parent.Parent).Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle];
}
DataContext = ViewModel;
}
}
private void Grid_PointerEntered(object sender, PointerEventArgs e)
{
if (sender is Grid grid)
{
if (grid.DataContext is UserProfile profile)
{
profile.IsPointerOver = true;
}
}
}
private void Grid_OnPointerExited(object sender, PointerEventArgs e)
{
if (sender is Grid grid)
{
if (grid.DataContext is UserProfile profile)
{
profile.IsPointerOver = false;
}
}
}
private void ProfilesList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is ListBox listBox)
{
int selectedIndex = listBox.SelectedIndex;
if (selectedIndex >= 0 && selectedIndex < ViewModel.Profiles.Count)
{
if (ViewModel.Profiles[selectedIndex] is UserProfile userProfile)
{
_parent?.AccountManager?.OpenUser(userProfile.UserId);
foreach (BaseModel profile in ViewModel.Profiles)
{
if (profile is UserProfile uProfile)
{
uProfile.UpdateState();
}
}
}
}
}
}
private void AddUser(object sender, RoutedEventArgs e)
{
_parent.AddUser();
}
private void EditUser(object sender, RoutedEventArgs e)
{
if (sender is Avalonia.Controls.Button button)
{
if (button.DataContext is UserProfile userProfile)
{
_parent.EditUser(userProfile);
}
}
}
private void ManageSaves(object sender, RoutedEventArgs e)
{
_parent.ManageSaves();
}
private void RecoverLostAccounts(object sender, RoutedEventArgs e)
{
_parent.RecoverLostAccounts();
}
private void Close(object sender, RoutedEventArgs e)
{
((ContentDialog)_parent.Parent).Hide();
}
}
}

View File

@@ -1,54 +0,0 @@
<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:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
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" d:DesignWidth="400" d:DesignHeight="350"
x:Class="Ryujinx.Ava.UI.Windows.AvatarWindow"
Margin="0"
Padding="0"
x:CompileBindings="True"
x:DataType="viewModels:AvatarProfileViewModel"
Focusable="True">
<Design.DataContext>
<viewModels:AvatarProfileViewModel />
</Design.DataContext>
<UserControl.Resources>
<helpers:BitmapArrayValueConverter x:Key="ByteImage" />
</UserControl.Resources>
<Grid Margin="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox Grid.Row="1" BorderThickness="0" SelectedIndex="{Binding SelectedIndex}" Height="400"
Items="{Binding Images}" HorizontalAlignment="Stretch" VerticalAlignment="Center">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" MaxWidth="700" Margin="0" HorizontalAlignment="Center" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Image Margin="5" Height="96" Width="96"
Source="{Binding Data, Converter={StaticResource ByteImage}}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<ProgressBar Grid.Row="2" IsIndeterminate="{Binding IsIndeterminate}" Value="{Binding ImagesLoaded}" HorizontalAlignment="Stretch" Margin="5"
Maximum="{Binding ImageCount}" Minimum="0" />
<StackPanel Grid.Row="3" Orientation="Horizontal" Spacing="10" Margin="10" HorizontalAlignment="Center">
<Button Content="{locale:Locale AvatarChoose}" Width="200" Name="ChooseButton" Click="ChooseButton_OnClick" />
<ui:ColorPickerButton Color="{Binding BackgroundColor, Mode=TwoWay}" Name="ColorButton" />
<Button HorizontalAlignment="Right" Content="{locale:Locale Discard}" Click="CloseButton_OnClick"
Name="CloseButton"
Width="200" />
</StackPanel>
</Grid>
</UserControl>

View File

@@ -44,7 +44,8 @@
<settings:SettingsNetworkView Name="NetworkPage" /> <settings:SettingsNetworkView Name="NetworkPage" />
<settings:SettingsLoggingView Name="LoggingPage" /> <settings:SettingsLoggingView Name="LoggingPage" />
</Grid> </Grid>
<ui:NavigationView Grid.Row="1" <ui:NavigationView
Grid.Row="1"
IsSettingsVisible="False" IsSettingsVisible="False"
Name="NavPanel" Name="NavPanel"
IsBackEnabled="False" IsBackEnabled="False"
@@ -54,7 +55,8 @@
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
OpenPaneLength="200"> OpenPaneLength="200">
<ui:NavigationView.MenuItems> <ui:NavigationView.MenuItems>
<ui:NavigationViewItem IsSelected="True" <ui:NavigationViewItem
IsSelected="True"
Content="{locale:Locale SettingsTabGeneral}" Content="{locale:Locale SettingsTabGeneral}"
Tag="UiPage" Tag="UiPage"
Icon="New" /> Icon="New" />
@@ -74,7 +76,8 @@
Content="{locale:Locale SettingsTabCpu}" Content="{locale:Locale SettingsTabCpu}"
Tag="CpuPage"> Tag="CpuPage">
<ui:NavigationViewItem.Icon> <ui:NavigationViewItem.Icon>
<ui:FontIcon FontFamily="avares://Ryujinx.Ava/Assets/Fonts#Segoe Fluent Icons" <ui:FontIcon
FontFamily="avares://Ryujinx.Ava/Assets/Fonts#Segoe Fluent Icons"
Glyph="{helpers:GlyphValueConverter Chip}" /> Glyph="{helpers:GlyphValueConverter Chip}" />
</ui:NavigationViewItem.Icon> </ui:NavigationViewItem.Icon>
</ui:NavigationViewItem> </ui:NavigationViewItem>
@@ -95,6 +98,11 @@
Tag="LoggingPage" Tag="LoggingPage"
Icon="Document" /> Icon="Document" />
</ui:NavigationView.MenuItems> </ui:NavigationView.MenuItems>
<ui:NavigationView.Styles>
<Style Selector="Grid#PlaceholderGrid">
<Setter Property="Height" Value="40" />
</Style>
</ui:NavigationView.Styles>
</ui:NavigationView> </ui:NavigationView>
<ReversibleStackPanel <ReversibleStackPanel
Grid.Row="2" Grid.Row="2"

View File

@@ -1,115 +1,135 @@
<window:StyleableWindow <UserControl
x:Class="Ryujinx.Ava.UI.Windows.TitleUpdateWindow" x:Class="Ryujinx.Ava.UI.Windows.TitleUpdateWindow"
xmlns="https://github.com/avaloniaui" xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
Width="600" xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
Height="400" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
MinWidth="600" Width="500"
MinHeight="400" Height="300"
MaxWidth="600"
MaxHeight="400"
SizeToContent="Height"
WindowStartupLocation="CenterOwner"
mc:Ignorable="d" mc:Ignorable="d"
x:CompileBindings="True"
x:DataType="viewModels:TitleUpdateViewModel"
Focusable="True"> Focusable="True">
<Grid Margin="15"> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBlock
Name="Heading"
Grid.Row="1"
MaxWidth="500"
Margin="20,15,20,20"
HorizontalAlignment="Center"
VerticalAlignment="Center"
LineHeight="18"
TextAlignment="Center"
TextWrapping="Wrap" />
<Border <Border
Grid.Row="2" Grid.Row="0"
Margin="5" Margin="0 0 0 24"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
BorderBrush="Gray" BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
BorderThickness="1"> BorderThickness="1"
<ScrollViewer CornerRadius="5"
VerticalAlignment="Stretch" Padding="2.5">
HorizontalScrollBarVisibility="Auto" <ListBox
VerticalScrollBarVisibility="Auto"> VirtualizationMode="None"
<ItemsControl Background="Transparent"
Margin="10" SelectedItem="{Binding SelectedUpdate, Mode=TwoWay}"
HorizontalAlignment="Stretch" Items="{Binding Views}">
VerticalAlignment="Stretch" <ListBox.DataTemplates>
Items="{Binding _titleUpdates}"> <DataTemplate
<ItemsControl.ItemTemplate> DataType="models:TitleUpdateModel">
<DataTemplate> <Panel Margin="10">
<RadioButton <TextBlock
Padding="8,0" HorizontalAlignment="Left"
VerticalContentAlignment="Center"
GroupName="Update"
IsChecked="{Binding IsEnabled, Mode=TwoWay}">
<Label
Margin="0"
VerticalAlignment="Center" VerticalAlignment="Center"
Content="{Binding Label}" TextWrapping="Wrap"
FontSize="12" /> Text="{Binding Label}" />
</RadioButton> <StackPanel
Spacing="10"
Orientation="Horizontal"
HorizontalAlignment="Right">
<Button
VerticalAlignment="Center"
HorizontalAlignment="Right"
Padding="10"
MinWidth="0"
MinHeight="0"
Click="OpenLocation">
<ui:SymbolIcon
Symbol="OpenFolder"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Button>
<Button
VerticalAlignment="Center"
HorizontalAlignment="Right"
Padding="10"
MinWidth="0"
MinHeight="0"
Click="RemoveUpdate">
<ui:SymbolIcon
Symbol="Cancel"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Button>
</StackPanel>
</Panel>
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> <DataTemplate
</ItemsControl> DataType="viewModels:BaseModel">
</ScrollViewer> <Panel
Height="33"
Margin="10">
<TextBlock
HorizontalAlignment="Left"
VerticalAlignment="Center"
TextWrapping="Wrap"
Text="{locale:Locale NoUpdate}" />
</Panel>
</DataTemplate>
</ListBox.DataTemplates>
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Background" Value="Transparent" />
</Style>
</ListBox.Styles>
</ListBox>
</Border> </Border>
<DockPanel <Panel
Grid.Row="3" Grid.Row="1"
Margin="0"
HorizontalAlignment="Stretch"> HorizontalAlignment="Stretch">
<DockPanel Margin="0" HorizontalAlignment="Left"> <StackPanel
Orientation="Horizontal"
Spacing="10"
HorizontalAlignment="Left">
<Button <Button
Name="AddButton" Name="AddButton"
MinWidth="90" MinWidth="90"
Margin="5" Command="{ReflectionBinding Add}">
Command="{Binding Add}">
<TextBlock Text="{locale:Locale SettingsTabGeneralAdd}" /> <TextBlock Text="{locale:Locale SettingsTabGeneralAdd}" />
</Button> </Button>
<Button
Name="RemoveButton"
MinWidth="90"
Margin="5"
Command="{Binding RemoveSelected}">
<TextBlock Text="{locale:Locale SettingsTabGeneralRemove}" />
</Button>
<Button <Button
Name="RemoveAllButton" Name="RemoveAllButton"
MinWidth="90" MinWidth="90"
Margin="5" Click="RemoveAll">
Command="{Binding RemoveAll}">
<TextBlock Text="{locale:Locale DlcManagerRemoveAllButton}" /> <TextBlock Text="{locale:Locale DlcManagerRemoveAllButton}" />
</Button> </Button>
</DockPanel> </StackPanel>
<DockPanel Margin="0" HorizontalAlignment="Right"> <StackPanel
Orientation="Horizontal"
Spacing="10"
HorizontalAlignment="Right">
<Button <Button
Name="SaveButton" Name="SaveButton"
MinWidth="90" MinWidth="90"
Margin="5" Click="Save">
Command="{Binding Save}">
<TextBlock Text="{locale:Locale SettingsButtonSave}" /> <TextBlock Text="{locale:Locale SettingsButtonSave}" />
</Button> </Button>
<Button <Button
Name="CancelButton" Name="CancelButton"
MinWidth="90" MinWidth="90"
Margin="5" Click="Close">
Command="{Binding Close}">
<TextBlock Text="{locale:Locale InputDialogCancel}" /> <TextBlock Text="{locale:Locale InputDialogCancel}" />
</Button> </Button>
</DockPanel> </StackPanel>
</DockPanel> </Panel>
</Grid> </Grid>
</window:StyleableWindow> </UserControl>

View File

@@ -1,271 +1,116 @@
using Avalonia.Collections;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Threading; using Avalonia.Interactivity;
using LibHac.Common; using Avalonia.Styling;
using LibHac.Fs; using FluentAvalonia.UI.Controls;
using LibHac.Fs.Fsa;
using LibHac.FsSystem;
using LibHac.Ns;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.Models;
using Ryujinx.Common.Configuration; using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Common.Utilities; using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS; using Ryujinx.Ui.Common.Helper;
using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Text; using System.Text;
using Path = System.IO.Path; using System.Threading.Tasks;
using SpanHelpers = LibHac.Common.SpanHelpers; using Button = Avalonia.Controls.Button;
namespace Ryujinx.Ava.UI.Windows namespace Ryujinx.Ava.UI.Windows
{ {
public partial class TitleUpdateWindow : StyleableWindow public partial class TitleUpdateWindow : UserControl
{ {
private readonly string _titleUpdateJsonPath; public TitleUpdateViewModel ViewModel;
private TitleUpdateMetadata _titleUpdateWindowData;
private VirtualFileSystem _virtualFileSystem { get; }
private AvaloniaList<TitleUpdateModel> _titleUpdates { get; set; }
private ulong _titleId { get; }
private string _titleName { get; }
public TitleUpdateWindow() public TitleUpdateWindow()
{ {
DataContext = this; DataContext = this;
InitializeComponent(); InitializeComponent();
Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance[LocaleKeys.UpdateWindowTitle]} - {_titleName} ({_titleId:X16})";
} }
public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
{ {
_virtualFileSystem = virtualFileSystem; DataContext = ViewModel = new TitleUpdateViewModel(virtualFileSystem, titleId, titleName);
_titleUpdates = new AvaloniaList<TitleUpdateModel>();
_titleId = titleId;
_titleName = titleName;
_titleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "updates.json");
try
{
_titleUpdateWindowData = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(_titleUpdateJsonPath);
}
catch
{
_titleUpdateWindowData = new TitleUpdateMetadata
{
Selected = "",
Paths = new List<string>()
};
}
DataContext = this;
InitializeComponent(); InitializeComponent();
Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance[LocaleKeys.UpdateWindowTitle]} - {_titleName} ({_titleId:X16})";
LoadUpdates();
PrintHeading();
} }
private void PrintHeading() public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
{ {
Heading.Text = string.Format(LocaleManager.Instance[LocaleKeys.GameUpdateWindowHeading], _titleUpdates.Count, _titleName, _titleId.ToString("X16")); ContentDialog contentDialog = new()
}
private void LoadUpdates()
{ {
_titleUpdates.Add(new TitleUpdateModel(default, string.Empty, true)); PrimaryButtonText = "",
SecondaryButtonText = "",
foreach (string path in _titleUpdateWindowData.Paths) CloseButtonText = "",
{ Content = new TitleUpdateWindow(virtualFileSystem, titleId, titleName),
AddUpdate(path); Title = string.Format(LocaleManager.Instance[LocaleKeys.GameUpdateWindowHeading], titleName, titleId.ToString("X16"))
}
if (_titleUpdateWindowData.Selected == "")
{
_titleUpdates[0].IsEnabled = true;
}
else
{
TitleUpdateModel selected = _titleUpdates.FirstOrDefault(x => x.Path == _titleUpdateWindowData.Selected);
List<TitleUpdateModel> enabled = _titleUpdates.Where(x => x.IsEnabled).ToList();
foreach (TitleUpdateModel update in enabled)
{
update.IsEnabled = false;
}
if (selected != null)
{
selected.IsEnabled = true;
}
}
SortUpdates();
}
private void AddUpdate(string path)
{
if (File.Exists(path) && !_titleUpdates.Any(x => x.Path == path))
{
using FileStream file = new(path, FileMode.Open, FileAccess.Read);
try
{
(Nca patchNca, Nca controlNca) = ApplicationLoader.GetGameUpdateDataFromPartition(_virtualFileSystem, new PartitionFileSystem(file.AsStorage()), _titleId.ToString("x16"), 0);
if (controlNca != null && patchNca != null)
{
ApplicationControlProperty controlData = new();
using UniqueRef<IFile> nacpFile = new();
controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref(), "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure();
_titleUpdates.Add(new TitleUpdateModel(controlData, path));
foreach (var update in _titleUpdates)
{
update.IsEnabled = false;
}
_titleUpdates.Last().IsEnabled = true;
}
else
{
Dispatcher.UIThread.Post(async () =>
{
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]);
});
}
}
catch (Exception ex)
{
Dispatcher.UIThread.Post(async () =>
{
await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[LocaleKeys.DialogDlcLoadNcaErrorMessage], ex.Message, path));
});
}
}
}
private void RemoveUpdates(bool removeSelectedOnly = false)
{
if (removeSelectedOnly)
{
_titleUpdates.RemoveAll(_titleUpdates.Where(x => x.IsEnabled && !x.IsNoUpdate).ToList());
}
else
{
_titleUpdates.RemoveAll(_titleUpdates.Where(x => !x.IsNoUpdate).ToList());
}
_titleUpdates.FirstOrDefault(x => x.IsNoUpdate).IsEnabled = true;
SortUpdates();
PrintHeading();
}
public void RemoveSelected()
{
RemoveUpdates(true);
}
public void RemoveAll()
{
RemoveUpdates();
}
public async void Add()
{
OpenFileDialog dialog = new()
{
Title = LocaleManager.Instance[LocaleKeys.SelectUpdateDialogTitle],
AllowMultiple = true
}; };
dialog.Filters.Add(new FileDialogFilter Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>());
{ bottomBorder.Setters.Add(new Setter(IsVisibleProperty, false));
Name = "NSP",
Extensions = { "nsp" }
});
string[] files = await dialog.ShowAsync(this); contentDialog.Styles.Add(bottomBorder);
if (files != null) await ContentDialogHelper.ShowAsync(contentDialog);
}
private void Close(object sender, RoutedEventArgs e)
{ {
foreach (string file in files) ((ContentDialog)Parent).Hide();
}
public void Save(object sender, RoutedEventArgs e)
{ {
AddUpdate(file); ViewModel._titleUpdateWindowData.Paths.Clear();
ViewModel._titleUpdateWindowData.Selected = "";
foreach (TitleUpdateModel update in ViewModel.TitleUpdates)
{
ViewModel._titleUpdateWindowData.Paths.Add(update.Path);
if (update == ViewModel.SelectedUpdate)
{
ViewModel._titleUpdateWindowData.Selected = update.Path;
} }
} }
SortUpdates(); using (FileStream titleUpdateJsonStream = File.Create(ViewModel._titleUpdateJsonPath, 4096, FileOptions.WriteThrough))
PrintHeading(); {
titleUpdateJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(ViewModel._titleUpdateWindowData, true)));
} }
private void SortUpdates() if (VisualRoot is MainWindow window)
{
var list = _titleUpdates.ToList();
list.Sort((first, second) =>
{
if (string.IsNullOrEmpty(first.Control.DisplayVersionString.ToString()))
{
return -1;
}
else if (string.IsNullOrEmpty(second.Control.DisplayVersionString.ToString()))
{
return 1;
}
return Version.Parse(first.Control.DisplayVersionString.ToString()).CompareTo(Version.Parse(second.Control.DisplayVersionString.ToString())) * -1;
});
_titleUpdates.Clear();
_titleUpdates.AddRange(list);
}
public void Save()
{
_titleUpdateWindowData.Paths.Clear();
_titleUpdateWindowData.Selected = "";
foreach (TitleUpdateModel update in _titleUpdates)
{
_titleUpdateWindowData.Paths.Add(update.Path);
if (update.IsEnabled)
{
_titleUpdateWindowData.Selected = update.Path;
}
}
using (FileStream titleUpdateJsonStream = File.Create(_titleUpdateJsonPath, 4096, FileOptions.WriteThrough))
{
titleUpdateJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_titleUpdateWindowData, true)));
}
if (Owner is MainWindow window)
{ {
window.ViewModel.LoadApplications(); window.ViewModel.LoadApplications();
} }
Close(); ((ContentDialog)Parent).Hide();
}
private void OpenLocation(object sender, RoutedEventArgs e)
{
if (sender is Button button)
{
if (button.DataContext is TitleUpdateModel model)
{
OpenHelper.LocateFile(model.Path);
}
}
}
private void RemoveUpdate(object sender, RoutedEventArgs e)
{
if (sender is Button button)
{
ViewModel.RemoveUpdate((TitleUpdateModel)button.DataContext);
}
}
private void RemoveAll(object sender, RoutedEventArgs e)
{
ViewModel.TitleUpdates.Clear();
ViewModel.SortUpdates();
} }
} }
} }

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Globalization;
namespace Ryujinx.Common.Utilities namespace Ryujinx.Common.Utilities
{ {
@@ -6,7 +7,7 @@ namespace Ryujinx.Common.Utilities
{ {
public static UInt128 FromHex(string hex) public static UInt128 FromHex(string hex)
{ {
return new UInt128((ulong)Convert.ToInt64(hex.Substring(0, 16), 16), (ulong)Convert.ToInt64(hex.Substring(16), 16)); return new UInt128(ulong.Parse(hex.AsSpan(0, 16), NumberStyles.HexNumber), ulong.Parse(hex.AsSpan(16), NumberStyles.HexNumber));
} }
public static UInt128 CreateRandom() public static UInt128 CreateRandom()

470
Ryujinx.Cpu/AddressSpace.cs Normal file
View File

@@ -0,0 +1,470 @@
using Ryujinx.Common;
using Ryujinx.Common.Collections;
using Ryujinx.Memory;
using System;
namespace Ryujinx.Cpu
{
class AddressSpace : IDisposable
{
private const ulong PageSize = 0x1000;
private const int DefaultBlockAlignment = 1 << 20;
private enum MappingType : byte
{
None,
Private,
Shared
}
private class Mapping : IntrusiveRedBlackTreeNode<Mapping>, IComparable<Mapping>
{
public ulong Address { get; private set; }
public ulong Size { get; private set; }
public ulong EndAddress => Address + Size;
public MappingType Type { get; private set; }
public Mapping(ulong address, ulong size, MappingType type)
{
Address = address;
Size = size;
Type = type;
}
public Mapping Split(ulong splitAddress)
{
ulong leftSize = splitAddress - Address;
ulong rightSize = EndAddress - splitAddress;
Mapping left = new Mapping(Address, leftSize, Type);
Address = splitAddress;
Size = rightSize;
return left;
}
public void UpdateState(MappingType newType)
{
Type = newType;
}
public void Extend(ulong sizeDelta)
{
Size += sizeDelta;
}
public int CompareTo(Mapping other)
{
if (Address < other.Address)
{
return -1;
}
else if (Address <= other.EndAddress - 1UL)
{
return 0;
}
else
{
return 1;
}
}
}
private class PrivateMapping : IntrusiveRedBlackTreeNode<PrivateMapping>, IComparable<PrivateMapping>
{
public ulong Address { get; private set; }
public ulong Size { get; private set; }
public ulong EndAddress => Address + Size;
public PrivateMemoryAllocation PrivateAllocation { get; private set; }
public PrivateMapping(ulong address, ulong size, PrivateMemoryAllocation privateAllocation)
{
Address = address;
Size = size;
PrivateAllocation = privateAllocation;
}
public PrivateMapping Split(ulong splitAddress)
{
ulong leftSize = splitAddress - Address;
ulong rightSize = EndAddress - splitAddress;
(var leftAllocation, PrivateAllocation) = PrivateAllocation.Split(leftSize);
PrivateMapping left = new PrivateMapping(Address, leftSize, leftAllocation);
Address = splitAddress;
Size = rightSize;
return left;
}
public void Map(MemoryBlock baseBlock, MemoryBlock mirrorBlock, PrivateMemoryAllocation newAllocation)
{
baseBlock.MapView(newAllocation.Memory, newAllocation.Offset, Address, Size);
mirrorBlock.MapView(newAllocation.Memory, newAllocation.Offset, Address, Size);
PrivateAllocation = newAllocation;
}
public void Unmap(MemoryBlock baseBlock, MemoryBlock mirrorBlock)
{
if (PrivateAllocation.IsValid)
{
baseBlock.UnmapView(PrivateAllocation.Memory, Address, Size);
mirrorBlock.UnmapView(PrivateAllocation.Memory, Address, Size);
PrivateAllocation.Dispose();
}
PrivateAllocation = default;
}
public void Extend(ulong sizeDelta)
{
Size += sizeDelta;
}
public int CompareTo(PrivateMapping other)
{
if (Address < other.Address)
{
return -1;
}
else if (Address <= other.EndAddress - 1UL)
{
return 0;
}
else
{
return 1;
}
}
}
private readonly MemoryBlock _backingMemory;
private readonly PrivateMemoryAllocator _privateMemoryAllocator;
private readonly IntrusiveRedBlackTree<Mapping> _mappingTree;
private readonly IntrusiveRedBlackTree<PrivateMapping> _privateTree;
private readonly object _treeLock;
private readonly bool _supports4KBPages;
public MemoryBlock Base { get; }
public MemoryBlock Mirror { get; }
public AddressSpace(MemoryBlock backingMemory, ulong asSize, bool supports4KBPages)
{
if (!supports4KBPages)
{
_privateMemoryAllocator = new PrivateMemoryAllocator(DefaultBlockAlignment, MemoryAllocationFlags.Mirrorable | MemoryAllocationFlags.NoMap);
_mappingTree = new IntrusiveRedBlackTree<Mapping>();
_privateTree = new IntrusiveRedBlackTree<PrivateMapping>();
_treeLock = new object();
_mappingTree.Add(new Mapping(0UL, asSize, MappingType.None));
_privateTree.Add(new PrivateMapping(0UL, asSize, default));
}
_backingMemory = backingMemory;
_supports4KBPages = supports4KBPages;
MemoryAllocationFlags asFlags = MemoryAllocationFlags.Reserve | MemoryAllocationFlags.ViewCompatible;
Base = new MemoryBlock(asSize, asFlags);
Mirror = new MemoryBlock(asSize, asFlags);
}
public void Map(ulong va, ulong pa, ulong size, MemoryMapFlags flags)
{
if (_supports4KBPages)
{
Base.MapView(_backingMemory, pa, va, size);
Mirror.MapView(_backingMemory, pa, va, size);
return;
}
lock (_treeLock)
{
ulong alignment = MemoryBlock.GetPageSize();
bool isAligned = ((va | pa | size) & (alignment - 1)) == 0;
if (flags.HasFlag(MemoryMapFlags.Private) && !isAligned)
{
Update(va, pa, size, MappingType.Private);
}
else
{
// The update method assumes that shared mappings are already aligned.
if (!flags.HasFlag(MemoryMapFlags.Private))
{
if ((va & (alignment - 1)) != (pa & (alignment - 1)))
{
throw new InvalidMemoryRegionException($"Virtual address 0x{va:X} and physical address 0x{pa:X} are misaligned and can't be aligned.");
}
ulong endAddress = va + size;
va = BitUtils.AlignDown(va, alignment);
pa = BitUtils.AlignDown(pa, alignment);
size = BitUtils.AlignUp(endAddress, alignment) - va;
}
Update(va, pa, size, MappingType.Shared);
}
}
}
public void Unmap(ulong va, ulong size)
{
if (_supports4KBPages)
{
Base.UnmapView(_backingMemory, va, size);
Mirror.UnmapView(_backingMemory, va, size);
return;
}
lock (_treeLock)
{
Update(va, 0UL, size, MappingType.None);
}
}
private void Update(ulong va, ulong pa, ulong size, MappingType type)
{
Mapping map = _mappingTree.GetNode(new Mapping(va, 1UL, MappingType.None));
Update(map, va, pa, size, type);
}
private Mapping Update(Mapping map, ulong va, ulong pa, ulong size, MappingType type)
{
ulong endAddress = va + size;
for (; map != null; map = map.Successor)
{
if (map.Address < va)
{
_mappingTree.Add(map.Split(va));
}
if (map.EndAddress > endAddress)
{
Mapping newMap = map.Split(endAddress);
_mappingTree.Add(newMap);
map = newMap;
}
switch (type)
{
case MappingType.None:
if (map.Type == MappingType.Shared)
{
ulong startOffset = map.Address - va;
ulong mapVa = va + startOffset;
ulong mapSize = Math.Min(size - startOffset, map.Size);
ulong mapEndAddress = mapVa + mapSize;
ulong alignment = MemoryBlock.GetPageSize();
mapVa = BitUtils.AlignDown(mapVa, alignment);
mapEndAddress = BitUtils.AlignUp(mapEndAddress, alignment);
mapSize = mapEndAddress - mapVa;
Base.UnmapView(_backingMemory, mapVa, mapSize);
Mirror.UnmapView(_backingMemory, mapVa, mapSize);
}
else
{
UnmapPrivate(va, size);
}
break;
case MappingType.Private:
if (map.Type == MappingType.Shared)
{
throw new InvalidMemoryRegionException($"Private mapping request at 0x{va:X} with size 0x{size:X} overlaps shared mapping at 0x{map.Address:X} with size 0x{map.Size:X}.");
}
else
{
MapPrivate(va, size);
}
break;
case MappingType.Shared:
if (map.Type != MappingType.None)
{
throw new InvalidMemoryRegionException($"Shared mapping request at 0x{va:X} with size 0x{size:X} overlaps mapping at 0x{map.Address:X} with size 0x{map.Size:X}.");
}
else
{
ulong startOffset = map.Address - va;
ulong mapPa = pa + startOffset;
ulong mapVa = va + startOffset;
ulong mapSize = Math.Min(size - startOffset, map.Size);
Base.MapView(_backingMemory, mapPa, mapVa, mapSize);
Mirror.MapView(_backingMemory, mapPa, mapVa, mapSize);
}
break;
}
map.UpdateState(type);
map = TryCoalesce(map);
if (map.EndAddress >= endAddress)
{
break;
}
}
return map;
}
private Mapping TryCoalesce(Mapping map)
{
Mapping previousMap = map.Predecessor;
Mapping nextMap = map.Successor;
if (previousMap != null && CanCoalesce(previousMap, map))
{
previousMap.Extend(map.Size);
_mappingTree.Remove(map);
map = previousMap;
}
if (nextMap != null && CanCoalesce(map, nextMap))
{
map.Extend(nextMap.Size);
_mappingTree.Remove(nextMap);
}
return map;
}
private static bool CanCoalesce(Mapping left, Mapping right)
{
return left.Type == right.Type;
}
private void MapPrivate(ulong va, ulong size)
{
ulong endAddress = va + size;
ulong alignment = MemoryBlock.GetPageSize();
// Expand the range outwards based on page size to ensure that at least the requested region is mapped.
ulong vaAligned = BitUtils.AlignDown(va, alignment);
ulong endAddressAligned = BitUtils.AlignUp(endAddress, alignment);
ulong sizeAligned = endAddressAligned - vaAligned;
PrivateMapping map = _privateTree.GetNode(new PrivateMapping(va, 1UL, default));
for (; map != null; map = map.Successor)
{
if (!map.PrivateAllocation.IsValid)
{
if (map.Address < vaAligned)
{
_privateTree.Add(map.Split(vaAligned));
}
if (map.EndAddress > endAddressAligned)
{
PrivateMapping newMap = map.Split(endAddressAligned);
_privateTree.Add(newMap);
map = newMap;
}
map.Map(Base, Mirror, _privateMemoryAllocator.Allocate(map.Size, MemoryBlock.GetPageSize()));
}
if (map.EndAddress >= endAddressAligned)
{
break;
}
}
}
private void UnmapPrivate(ulong va, ulong size)
{
ulong endAddress = va + size;
ulong alignment = MemoryBlock.GetPageSize();
// Shrink the range inwards based on page size to ensure we won't unmap memory that might be still in use.
ulong vaAligned = BitUtils.AlignUp(va, alignment);
ulong endAddressAligned = BitUtils.AlignDown(endAddress, alignment);
if (endAddressAligned <= vaAligned)
{
return;
}
ulong alignedSize = endAddressAligned - vaAligned;
PrivateMapping map = _privateTree.GetNode(new PrivateMapping(va, 1UL, default));
for (; map != null; map = map.Successor)
{
if (map.PrivateAllocation.IsValid)
{
if (map.Address < vaAligned)
{
_privateTree.Add(map.Split(vaAligned));
}
if (map.EndAddress > endAddressAligned)
{
PrivateMapping newMap = map.Split(endAddressAligned);
_privateTree.Add(newMap);
map = newMap;
}
map.Unmap(Base, Mirror);
map = TryCoalesce(map);
}
if (map.EndAddress >= endAddressAligned)
{
break;
}
}
}
private PrivateMapping TryCoalesce(PrivateMapping map)
{
PrivateMapping previousMap = map.Predecessor;
PrivateMapping nextMap = map.Successor;
if (previousMap != null && CanCoalesce(previousMap, map))
{
previousMap.Extend(map.Size);
_privateTree.Remove(map);
map = previousMap;
}
if (nextMap != null && CanCoalesce(map, nextMap))
{
map.Extend(nextMap.Size);
_privateTree.Remove(nextMap);
}
return map;
}
private static bool CanCoalesce(PrivateMapping left, PrivateMapping right)
{
return !left.PrivateAllocation.IsValid && !right.PrivateAllocation.IsValid;
}
public void Dispose()
{
_privateMemoryAllocator?.Dispose();
Base.Dispose();
Mirror.Dispose();
}
}
}

View File

@@ -7,5 +7,7 @@ namespace Ryujinx.Cpu.Jit
{ {
public IJitMemoryBlock Allocate(ulong size) => new JitMemoryBlock(size, MemoryAllocationFlags.None); public IJitMemoryBlock Allocate(ulong size) => new JitMemoryBlock(size, MemoryAllocationFlags.None);
public IJitMemoryBlock Reserve(ulong size) => new JitMemoryBlock(size, MemoryAllocationFlags.Reserve | MemoryAllocationFlags.Jit); public IJitMemoryBlock Reserve(ulong size) => new JitMemoryBlock(size, MemoryAllocationFlags.Reserve | MemoryAllocationFlags.Jit);
public ulong GetPageSize() => MemoryBlock.GetPageSize();
} }
} }

View File

@@ -28,6 +28,9 @@ namespace Ryujinx.Cpu.Jit
private readonly MemoryBlock _backingMemory; private readonly MemoryBlock _backingMemory;
private readonly InvalidAccessHandler _invalidAccessHandler; private readonly InvalidAccessHandler _invalidAccessHandler;
/// <inheritdoc/>
public bool Supports4KBPages => true;
/// <summary> /// <summary>
/// Address space width in bits. /// Address space width in bits.
/// </summary> /// </summary>
@@ -76,7 +79,7 @@ namespace Ryujinx.Cpu.Jit
} }
/// <inheritdoc/> /// <inheritdoc/>
public void Map(ulong va, ulong pa, ulong size) public void Map(ulong va, ulong pa, ulong size, MemoryMapFlags flags)
{ {
AssertValidAddressAndSize(va, size); AssertValidAddressAndSize(va, size);
@@ -91,9 +94,16 @@ namespace Ryujinx.Cpu.Jit
pa += PageSize; pa += PageSize;
remainingSize -= PageSize; remainingSize -= PageSize;
} }
Tracking.Map(oVa, size); Tracking.Map(oVa, size);
} }
/// <inheritdoc/>
public void MapForeign(ulong va, nuint hostPointer, ulong size)
{
throw new NotSupportedException();
}
/// <inheritdoc/> /// <inheritdoc/>
public void Unmap(ulong va, ulong size) public void Unmap(ulong va, ulong size)
{ {
@@ -378,6 +388,32 @@ namespace Ryujinx.Cpu.Jit
return true; return true;
} }
/// <inheritdoc/>
public IEnumerable<HostMemoryRange> GetHostRegions(ulong va, ulong size)
{
if (size == 0)
{
return Enumerable.Empty<HostMemoryRange>();
}
var guestRegions = GetPhysicalRegionsImpl(va, size);
if (guestRegions == null)
{
return null;
}
var regions = new HostMemoryRange[guestRegions.Count];
for (int i = 0; i < regions.Length; i++)
{
var guestRegion = guestRegions[i];
IntPtr pointer = _backingMemory.GetPointer(guestRegion.Address, guestRegion.Size);
regions[i] = new HostMemoryRange((nuint)(ulong)pointer, guestRegion.Size);
}
return regions;
}
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerable<MemoryRange> GetPhysicalRegions(ulong va, ulong size) public IEnumerable<MemoryRange> GetPhysicalRegions(ulong va, ulong size)
{ {
@@ -386,6 +422,11 @@ namespace Ryujinx.Cpu.Jit
return Enumerable.Empty<MemoryRange>(); return Enumerable.Empty<MemoryRange>();
} }
return GetPhysicalRegionsImpl(va, size);
}
private List<MemoryRange> GetPhysicalRegionsImpl(ulong va, ulong size)
{
if (!ValidateAddress(va) || !ValidateAddressAndSize(va, size)) if (!ValidateAddress(va) || !ValidateAddressAndSize(va, size))
{ {
return null; return null;

View File

@@ -5,6 +5,7 @@ using Ryujinx.Memory.Range;
using Ryujinx.Memory.Tracking; using Ryujinx.Memory.Tracking;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading; using System.Threading;
@@ -37,20 +38,21 @@ namespace Ryujinx.Cpu.Jit
private readonly InvalidAccessHandler _invalidAccessHandler; private readonly InvalidAccessHandler _invalidAccessHandler;
private readonly bool _unsafeMode; private readonly bool _unsafeMode;
private readonly MemoryBlock _addressSpace; private readonly AddressSpace _addressSpace;
private readonly MemoryBlock _addressSpaceMirror;
private readonly ulong _addressSpaceSize; private readonly ulong _addressSpaceSize;
private readonly MemoryBlock _backingMemory;
private readonly PageTable<ulong> _pageTable; private readonly PageTable<ulong> _pageTable;
private readonly MemoryEhMeilleure _memoryEh; private readonly MemoryEhMeilleure _memoryEh;
private readonly ulong[] _pageBitmap; private readonly ulong[] _pageBitmap;
/// <inheritdoc/>
public bool Supports4KBPages => MemoryBlock.GetPageSize() == PageSize;
public int AddressSpaceBits { get; } public int AddressSpaceBits { get; }
public IntPtr PageTablePointer => _addressSpace.Pointer; public IntPtr PageTablePointer => _addressSpace.Base.Pointer;
public MemoryManagerType Type => _unsafeMode ? MemoryManagerType.HostMappedUnsafe : MemoryManagerType.HostMapped; public MemoryManagerType Type => _unsafeMode ? MemoryManagerType.HostMappedUnsafe : MemoryManagerType.HostMapped;
@@ -67,7 +69,6 @@ namespace Ryujinx.Cpu.Jit
/// <param name="invalidAccessHandler">Optional function to handle invalid memory accesses</param> /// <param name="invalidAccessHandler">Optional function to handle invalid memory accesses</param>
public MemoryManagerHostMapped(MemoryBlock backingMemory, ulong addressSpaceSize, bool unsafeMode, InvalidAccessHandler invalidAccessHandler = null) public MemoryManagerHostMapped(MemoryBlock backingMemory, ulong addressSpaceSize, bool unsafeMode, InvalidAccessHandler invalidAccessHandler = null)
{ {
_backingMemory = backingMemory;
_pageTable = new PageTable<ulong>(); _pageTable = new PageTable<ulong>();
_invalidAccessHandler = invalidAccessHandler; _invalidAccessHandler = invalidAccessHandler;
_unsafeMode = unsafeMode; _unsafeMode = unsafeMode;
@@ -86,13 +87,10 @@ namespace Ryujinx.Cpu.Jit
_pageBitmap = new ulong[1 << (AddressSpaceBits - (PageBits + PageToPteShift))]; _pageBitmap = new ulong[1 << (AddressSpaceBits - (PageBits + PageToPteShift))];
MemoryAllocationFlags asFlags = MemoryAllocationFlags.Reserve | MemoryAllocationFlags.ViewCompatible; _addressSpace = new AddressSpace(backingMemory, asSize, Supports4KBPages);
_addressSpace = new MemoryBlock(asSize, asFlags); Tracking = new MemoryTracking(this, (int)MemoryBlock.GetPageSize(), invalidAccessHandler);
_addressSpaceMirror = new MemoryBlock(asSize, asFlags); _memoryEh = new MemoryEhMeilleure(_addressSpace.Base, _addressSpace.Mirror, Tracking);
Tracking = new MemoryTracking(this, PageSize, invalidAccessHandler);
_memoryEh = new MemoryEhMeilleure(_addressSpace, _addressSpaceMirror, Tracking);
} }
/// <summary> /// <summary>
@@ -145,18 +143,23 @@ namespace Ryujinx.Cpu.Jit
} }
/// <inheritdoc/> /// <inheritdoc/>
public void Map(ulong va, ulong pa, ulong size) public void Map(ulong va, ulong pa, ulong size, MemoryMapFlags flags)
{ {
AssertValidAddressAndSize(va, size); AssertValidAddressAndSize(va, size);
_addressSpace.MapView(_backingMemory, pa, va, size); _addressSpace.Map(va, pa, size, flags);
_addressSpaceMirror.MapView(_backingMemory, pa, va, size);
AddMapping(va, size); AddMapping(va, size);
PtMap(va, pa, size); PtMap(va, pa, size);
Tracking.Map(va, size); Tracking.Map(va, size);
} }
/// <inheritdoc/>
public void MapForeign(ulong va, nuint hostPointer, ulong size)
{
throw new NotSupportedException();
}
/// <inheritdoc/> /// <inheritdoc/>
public void Unmap(ulong va, ulong size) public void Unmap(ulong va, ulong size)
{ {
@@ -167,8 +170,7 @@ namespace Ryujinx.Cpu.Jit
RemoveMapping(va, size); RemoveMapping(va, size);
PtUnmap(va, size); PtUnmap(va, size);
_addressSpace.UnmapView(_backingMemory, va, size); _addressSpace.Unmap(va, size);
_addressSpaceMirror.UnmapView(_backingMemory, va, size);
} }
private void PtMap(ulong va, ulong pa, ulong size) private void PtMap(ulong va, ulong pa, ulong size)
@@ -201,7 +203,7 @@ namespace Ryujinx.Cpu.Jit
{ {
AssertMapped(va, (ulong)Unsafe.SizeOf<T>()); AssertMapped(va, (ulong)Unsafe.SizeOf<T>());
return _addressSpaceMirror.Read<T>(va); return _addressSpace.Mirror.Read<T>(va);
} }
catch (InvalidMemoryRegionException) catch (InvalidMemoryRegionException)
{ {
@@ -241,7 +243,7 @@ namespace Ryujinx.Cpu.Jit
{ {
AssertMapped(va, (ulong)data.Length); AssertMapped(va, (ulong)data.Length);
_addressSpaceMirror.Read(va, data); _addressSpace.Mirror.Read(va, data);
} }
catch (InvalidMemoryRegionException) catch (InvalidMemoryRegionException)
{ {
@@ -260,7 +262,7 @@ namespace Ryujinx.Cpu.Jit
{ {
SignalMemoryTracking(va, (ulong)Unsafe.SizeOf<T>(), write: true); SignalMemoryTracking(va, (ulong)Unsafe.SizeOf<T>(), write: true);
_addressSpaceMirror.Write(va, value); _addressSpace.Mirror.Write(va, value);
} }
catch (InvalidMemoryRegionException) catch (InvalidMemoryRegionException)
{ {
@@ -278,7 +280,7 @@ namespace Ryujinx.Cpu.Jit
{ {
SignalMemoryTracking(va, (ulong)data.Length, write: true); SignalMemoryTracking(va, (ulong)data.Length, write: true);
_addressSpaceMirror.Write(va, data); _addressSpace.Mirror.Write(va, data);
} }
catch (InvalidMemoryRegionException) catch (InvalidMemoryRegionException)
{ {
@@ -296,7 +298,7 @@ namespace Ryujinx.Cpu.Jit
{ {
AssertMapped(va, (ulong)data.Length); AssertMapped(va, (ulong)data.Length);
_addressSpaceMirror.Write(va, data); _addressSpace.Mirror.Write(va, data);
} }
catch (InvalidMemoryRegionException) catch (InvalidMemoryRegionException)
{ {
@@ -314,7 +316,7 @@ namespace Ryujinx.Cpu.Jit
{ {
SignalMemoryTracking(va, (ulong)data.Length, false); SignalMemoryTracking(va, (ulong)data.Length, false);
Span<byte> target = _addressSpaceMirror.GetSpan(va, data.Length); Span<byte> target = _addressSpace.Mirror.GetSpan(va, data.Length);
bool changed = !data.SequenceEqual(target); bool changed = !data.SequenceEqual(target);
if (changed) if (changed)
@@ -347,7 +349,7 @@ namespace Ryujinx.Cpu.Jit
AssertMapped(va, (ulong)size); AssertMapped(va, (ulong)size);
} }
return _addressSpaceMirror.GetSpan(va, size); return _addressSpace.Mirror.GetSpan(va, size);
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -362,7 +364,7 @@ namespace Ryujinx.Cpu.Jit
AssertMapped(va, (ulong)size); AssertMapped(va, (ulong)size);
} }
return _addressSpaceMirror.GetWritableRegion(va, size); return _addressSpace.Mirror.GetWritableRegion(va, size);
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -370,7 +372,7 @@ namespace Ryujinx.Cpu.Jit
{ {
SignalMemoryTracking(va, (ulong)Unsafe.SizeOf<T>(), true); SignalMemoryTracking(va, (ulong)Unsafe.SizeOf<T>(), true);
return ref _addressSpaceMirror.GetRef<T>(va); return ref _addressSpace.Mirror.GetRef<T>(va);
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -454,6 +456,14 @@ namespace Ryujinx.Cpu.Jit
return true; return true;
} }
/// <inheritdoc/>
public IEnumerable<HostMemoryRange> GetHostRegions(ulong va, ulong size)
{
AssertValidAddressAndSize(va, size);
return Enumerable.Repeat(new HostMemoryRange((nuint)(ulong)_addressSpace.Mirror.GetPointer(va, size), size), 1);
}
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerable<MemoryRange> GetPhysicalRegions(ulong va, ulong size) public IEnumerable<MemoryRange> GetPhysicalRegions(ulong va, ulong size)
{ {
@@ -692,7 +702,7 @@ namespace Ryujinx.Cpu.Jit
_ => MemoryPermission.None _ => MemoryPermission.None
}; };
_addressSpace.Reprotect(va, size, protection, false); _addressSpace.Base.Reprotect(va, size, protection, false);
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -799,7 +809,6 @@ namespace Ryujinx.Cpu.Jit
protected override void Destroy() protected override void Destroy()
{ {
_addressSpace.Dispose(); _addressSpace.Dispose();
_addressSpaceMirror.Dispose();
_memoryEh.Dispose(); _memoryEh.Dispose();
} }

View File

@@ -0,0 +1,41 @@
using Ryujinx.Memory;
using System;
namespace Ryujinx.Cpu
{
struct PrivateMemoryAllocation : IDisposable
{
private readonly PrivateMemoryAllocator _owner;
private readonly PrivateMemoryAllocator.Block _block;
public bool IsValid => _owner != null;
public MemoryBlock Memory => _block?.Memory;
public ulong Offset { get; }
public ulong Size { get; }
public PrivateMemoryAllocation(
PrivateMemoryAllocator owner,
PrivateMemoryAllocator.Block block,
ulong offset,
ulong size)
{
_owner = owner;
_block = block;
Offset = offset;
Size = size;
}
public (PrivateMemoryAllocation, PrivateMemoryAllocation) Split(ulong splitOffset)
{
PrivateMemoryAllocation left = new PrivateMemoryAllocation(_owner, _block, Offset, splitOffset);
PrivateMemoryAllocation right = new PrivateMemoryAllocation(_owner, _block, Offset + splitOffset, Size - splitOffset);
return (left, right);
}
public void Dispose()
{
_owner.Free(_block, Offset, Size);
}
}
}

View File

@@ -0,0 +1,268 @@
using Ryujinx.Common;
using Ryujinx.Memory;
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace Ryujinx.Cpu
{
class PrivateMemoryAllocator : PrivateMemoryAllocatorImpl<PrivateMemoryAllocator.Block>
{
public const ulong InvalidOffset = ulong.MaxValue;
public class Block : IComparable<Block>
{
public MemoryBlock Memory { get; private set; }
public ulong Size { get; }
private struct Range : IComparable<Range>
{
public ulong Offset { get; }
public ulong Size { get; }
public Range(ulong offset, ulong size)
{
Offset = offset;
Size = size;
}
public int CompareTo(Range other)
{
return Offset.CompareTo(other.Offset);
}
}
private readonly List<Range> _freeRanges;
public Block(MemoryBlock memory, ulong size)
{
Memory = memory;
Size = size;
_freeRanges = new List<Range>
{
new Range(0, size)
};
}
public ulong Allocate(ulong size, ulong alignment)
{
for (int i = 0; i < _freeRanges.Count; i++)
{
var range = _freeRanges[i];
ulong alignedOffset = BitUtils.AlignUp(range.Offset, alignment);
ulong sizeDelta = alignedOffset - range.Offset;
ulong usableSize = range.Size - sizeDelta;
if (sizeDelta < range.Size && usableSize >= size)
{
_freeRanges.RemoveAt(i);
if (sizeDelta != 0)
{
InsertFreeRange(range.Offset, sizeDelta);
}
ulong endOffset = range.Offset + range.Size;
ulong remainingSize = endOffset - (alignedOffset + size);
if (remainingSize != 0)
{
InsertFreeRange(endOffset - remainingSize, remainingSize);
}
return alignedOffset;
}
}
return InvalidOffset;
}
public void Free(ulong offset, ulong size)
{
InsertFreeRangeComingled(offset, size);
}
private void InsertFreeRange(ulong offset, ulong size)
{
var range = new Range(offset, size);
int index = _freeRanges.BinarySearch(range);
if (index < 0)
{
index = ~index;
}
_freeRanges.Insert(index, range);
}
private void InsertFreeRangeComingled(ulong offset, ulong size)
{
ulong endOffset = offset + size;
var range = new Range(offset, size);
int index = _freeRanges.BinarySearch(range);
if (index < 0)
{
index = ~index;
}
if (index < _freeRanges.Count && _freeRanges[index].Offset == endOffset)
{
endOffset = _freeRanges[index].Offset + _freeRanges[index].Size;
_freeRanges.RemoveAt(index);
}
if (index > 0 && _freeRanges[index - 1].Offset + _freeRanges[index - 1].Size == offset)
{
offset = _freeRanges[index - 1].Offset;
_freeRanges.RemoveAt(--index);
}
range = new Range(offset, endOffset - offset);
_freeRanges.Insert(index, range);
}
public bool IsTotallyFree()
{
if (_freeRanges.Count == 1 && _freeRanges[0].Size == Size)
{
Debug.Assert(_freeRanges[0].Offset == 0);
return true;
}
return false;
}
public int CompareTo(Block other)
{
return Size.CompareTo(other.Size);
}
public virtual void Destroy()
{
Memory.Dispose();
}
}
public PrivateMemoryAllocator(int blockAlignment, MemoryAllocationFlags allocationFlags) : base(blockAlignment, allocationFlags)
{
}
public PrivateMemoryAllocation Allocate(ulong size, ulong alignment)
{
var allocation = Allocate(size, alignment, CreateBlock);
return new PrivateMemoryAllocation(this, allocation.Block, allocation.Offset, allocation.Size);
}
private Block CreateBlock(MemoryBlock memory, ulong size)
{
return new Block(memory, size);
}
}
class PrivateMemoryAllocatorImpl<T> : IDisposable where T : PrivateMemoryAllocator.Block
{
private const ulong InvalidOffset = ulong.MaxValue;
public struct Allocation
{
public T Block { get; }
public ulong Offset { get; }
public ulong Size { get; }
public Allocation(T block, ulong offset, ulong size)
{
Block = block;
Offset = offset;
Size = size;
}
}
private readonly List<T> _blocks;
private readonly int _blockAlignment;
private readonly MemoryAllocationFlags _allocationFlags;
public PrivateMemoryAllocatorImpl(int blockAlignment, MemoryAllocationFlags allocationFlags)
{
_blocks = new List<T>();
_blockAlignment = blockAlignment;
_allocationFlags = allocationFlags;
}
protected Allocation Allocate(ulong size, ulong alignment, Func<MemoryBlock, ulong, T> createBlock)
{
// Ensure we have a sane alignment value.
if ((ulong)(int)alignment != alignment || (int)alignment <= 0)
{
throw new ArgumentOutOfRangeException(nameof(alignment), $"Invalid alignment 0x{alignment:X}.");
}
for (int i = 0; i < _blocks.Count; i++)
{
var block = _blocks[i];
if (block.Size >= size)
{
ulong offset = block.Allocate(size, alignment);
if (offset != InvalidOffset)
{
return new Allocation(block, offset, size);
}
}
}
ulong blockAlignedSize = BitUtils.AlignUp(size, (ulong)_blockAlignment);
var memory = new MemoryBlock(blockAlignedSize, _allocationFlags);
var newBlock = createBlock(memory, blockAlignedSize);
InsertBlock(newBlock);
ulong newBlockOffset = newBlock.Allocate(size, alignment);
Debug.Assert(newBlockOffset != InvalidOffset);
return new Allocation(newBlock, newBlockOffset, size);
}
public void Free(PrivateMemoryAllocator.Block block, ulong offset, ulong size)
{
block.Free(offset, size);
if (block.IsTotallyFree())
{
for (int i = 0; i < _blocks.Count; i++)
{
if (_blocks[i] == block)
{
_blocks.RemoveAt(i);
break;
}
}
block.Destroy();
}
}
private void InsertBlock(T block)
{
int index = _blocks.BinarySearch(block);
if (index < 0)
{
index = ~index;
}
_blocks.Insert(index, block);
}
public void Dispose()
{
for (int i = 0; i < _blocks.Count; i++)
{
_blocks[i].Destroy();
}
_blocks.Clear();
}
}
}

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