Compare commits

..

6 Commits

Author SHA1 Message Date
1faff14e73 UI: Fixes GTK sorting regression of #4294 2023-01-16 03:09:52 +01:00
784cf9d594 Ava UI: Renderer refactoring (#4297)
* Ava UI: `Renderer` refactoring

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

* Add spaces

* Fix TimeString formatting
2023-01-16 00:11:16 +01:00
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
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
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
31 changed files with 1348 additions and 1299 deletions

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;
@ -44,7 +44,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Runtime.Versioning;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop; using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop;
@ -66,8 +65,8 @@ namespace Ryujinx.Ava
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 InvisibleCursorWin;
private readonly IntPtr DefaultCursorWin; private readonly IntPtr DefaultCursorWin;
private readonly long _ticksPerFrame; private readonly long _ticksPerFrame;
private readonly Stopwatch _chrono; private readonly Stopwatch _chrono;
@ -80,6 +79,7 @@ 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;
@ -105,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; }
@ -117,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,
@ -144,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;
@ -183,10 +182,10 @@ namespace Ryujinx.Ava
{ {
_lastCursorMoveTime = Stopwatch.GetTimestamp(); _lastCursorMoveTime = Stopwatch.GetTimestamp();
if ((Renderer.Content as EmbeddedWindow).TransformedBounds != null) if (_rendererHost.EmbeddedWindow.TransformedBounds != null)
{ {
var point = e.GetCurrentPoint(window).Position; var point = e.GetCurrentPoint(window).Position;
var bounds = (Renderer.Content as EmbeddedWindow).TransformedBounds.Value.Clip; var bounds = _rendererHost.EmbeddedWindow.TransformedBounds.Value.Clip;
_isCursorInRenderer = point.X >= bounds.X && _isCursorInRenderer = point.X >= bounds.X &&
point.X <= bounds.Width + bounds.X && point.X <= bounds.Width + bounds.X &&
@ -318,7 +317,7 @@ namespace Ryujinx.Ava
_viewModel.SetUIProgressHandlers(Device); _viewModel.SetUIProgressHandlers(Device);
Renderer.SizeChanged += Window_SizeChanged; _rendererHost.SizeChanged += Window_SizeChanged;
_isActive = true; _isActive = true;
@ -430,11 +429,11 @@ 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)
@ -635,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
{ {
@ -787,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));
@ -827,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)
@ -837,7 +835,7 @@ namespace Ryujinx.Ava
} }
}); });
Renderer?.MakeCurrent(null); (_rendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.MakeCurrent(null);
} }
public void UpdateStatus() public void UpdateStatus()
@ -853,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)",

View File

@ -524,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",
@ -585,7 +585,7 @@
"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:",

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

@ -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,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,235 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Platform;
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;
NsView = IntPtr.Zero;
MetalLayer = IntPtr.Zero;
}
public EmbeddedWindow()
{
var stateObserverable = this.GetObservable(BoundsProperty);
stateObserverable.Subscribe(StateChanged);
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 = CreateArrowCursor()
};
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

@ -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

@ -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

@ -0,0 +1,258 @@
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")]
protected virtual IPlatformHandle CreateLinux(IPlatformHandle control)
{
if (ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.Vulkan)
{
X11Window = new GLXWindow(new NativeHandle(X11.DefaultDisplay), new NativeHandle(control.Handle));
}
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,69 @@
using Avalonia;
using Avalonia.Controls;
using Ryujinx.Common.Configuration;
using Ryujinx.Ui.Common.Configuration;
using Silk.NET.Vulkan;
using System;
namespace Ryujinx.Ava.UI.Renderer
{
public partial class RendererHost : UserControl, IDisposable
{
public EmbeddedWindow EmbeddedWindow;
public event EventHandler<EventArgs> WindowCreated;
public event Action<object, Size> SizeChanged;
public RendererHost()
{
InitializeComponent();
Dispose();
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;
}
}
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,17 +5,17 @@ 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)
{ {
_context = context; _context = context;
_window = window; _window = window;
} }
public void Dispose() public void Dispose()
@ -32,12 +32,12 @@ namespace Ryujinx.Ava.UI.Helpers
public static SPBOpenGLContext CreateBackgroundContext(OpenGLContextBase sharedContext) public static SPBOpenGLContext CreateBackgroundContext(OpenGLContextBase sharedContext)
{ {
OpenGLContextBase context = PlatformHelper.CreateOpenGLContext(FramebufferFormat.Default, 3, 3, OpenGLContextFlags.Compat, true, sharedContext); OpenGLContextBase context = PlatformHelper.CreateOpenGLContext(FramebufferFormat.Default, 3, 3, OpenGLContextFlags.Compat, true, sharedContext);
NativeWindowBase window = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100); NativeWindowBase window = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100);
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

@ -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;

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

@ -107,6 +107,7 @@
VerticalAlignment="Stretch"> VerticalAlignment="Stretch">
<ListBox <ListBox
Name="SaveList" Name="SaveList"
VirtualizationMode="None"
Items="{Binding Views}" Items="{Binding Views}"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"> VerticalAlignment="Stretch">
@ -116,6 +117,9 @@
<Setter Property="Margin" Value="5" /> <Setter Property="Margin" Value="5" />
<Setter Property="CornerRadius" Value="4" /> <Setter Property="CornerRadius" Value="4" />
</Style> </Style>
<Style Selector="ListBoxItem:selected /template/ Border#SelectionIndicator">
<Setter Property="IsVisible" Value="False" />
</Style>
</ListBox.Styles> </ListBox.Styles>
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate x:DataType="models:SaveModel"> <DataTemplate x:DataType="models:SaveModel">

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" VerticalAlignment="Center"
GroupName="Update" TextWrapping="Wrap"
IsChecked="{Binding IsEnabled, Mode=TwoWay}"> Text="{Binding Label}" />
<Label <StackPanel
Margin="0" Spacing="10"
Orientation="Horizontal"
HorizontalAlignment="Right">
<Button
VerticalAlignment="Center" VerticalAlignment="Center"
Content="{Binding Label}" HorizontalAlignment="Right"
FontSize="12" /> Padding="10"
</RadioButton> MinWidth="0"
</DataTemplate> MinHeight="0"
</ItemsControl.ItemTemplate> Click="OpenLocation">
</ItemsControl> <ui:SymbolIcon
</ScrollViewer> 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
DataType="viewModels:BaseModel">
<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 - 1, _titleName, _titleId.ToString("X16")); ContentDialog contentDialog = new()
}
private void LoadUpdates()
{
_titleUpdates.Add(new TitleUpdateModel(default, string.Empty, true));
foreach (string path in _titleUpdateWindowData.Paths)
{ {
AddUpdate(path); PrimaryButtonText = "",
} SecondaryButtonText = "",
CloseButtonText = "",
if (_titleUpdateWindowData.Selected == "") Content = new TitleUpdateWindow(virtualFileSystem, titleId, titleName),
{ Title = string.Format(LocaleManager.Instance[LocaleKeys.GameUpdateWindowHeading], titleName, titleId.ToString("X16"))
_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);
{
foreach (string file in files)
{
AddUpdate(file);
}
}
SortUpdates();
PrintHeading();
} }
private void SortUpdates() private void Close(object sender, RoutedEventArgs e)
{ {
var list = _titleUpdates.ToList(); ((ContentDialog)Parent).Hide();
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() public void Save(object sender, RoutedEventArgs e)
{ {
_titleUpdateWindowData.Paths.Clear(); ViewModel._titleUpdateWindowData.Paths.Clear();
_titleUpdateWindowData.Selected = ""; ViewModel._titleUpdateWindowData.Selected = "";
foreach (TitleUpdateModel update in _titleUpdates) foreach (TitleUpdateModel update in ViewModel.TitleUpdates)
{ {
_titleUpdateWindowData.Paths.Add(update.Path); ViewModel._titleUpdateWindowData.Paths.Add(update.Path);
if (update.IsEnabled) if (update == ViewModel.SelectedUpdate)
{ {
_titleUpdateWindowData.Selected = update.Path; ViewModel._titleUpdateWindowData.Selected = update.Path;
} }
} }
using (FileStream titleUpdateJsonStream = File.Create(_titleUpdateJsonPath, 4096, FileOptions.WriteThrough)) using (FileStream titleUpdateJsonStream = File.Create(ViewModel._titleUpdateJsonPath, 4096, FileOptions.WriteThrough))
{ {
titleUpdateJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_titleUpdateWindowData, true))); titleUpdateJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(ViewModel._titleUpdateWindowData, true)));
} }
if (Owner is MainWindow window) if (VisualRoot 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

@ -79,21 +79,7 @@ namespace Ryujinx.Graphics.Vulkan
var sampleCountFlags = ConvertToSampleCountFlags(gd.Capabilities.SupportedSampleCounts, (uint)info.Samples); var sampleCountFlags = ConvertToSampleCountFlags(gd.Capabilities.SupportedSampleCounts, (uint)info.Samples);
var usage = DefaultUsageFlags; var usage = GetImageUsageFromFormat(info.Format);
if (info.Format.IsDepthOrStencil())
{
usage |= ImageUsageFlags.DepthStencilAttachmentBit;
}
else if (info.Format.IsRtColorCompatible())
{
usage |= ImageUsageFlags.ColorAttachmentBit;
}
if (info.Format.IsImageCompatible())
{
usage |= ImageUsageFlags.StorageBit;
}
var flags = ImageCreateFlags.CreateMutableFormatBit; var flags = ImageCreateFlags.CreateMutableFormatBit;
@ -306,6 +292,27 @@ namespace Ryujinx.Graphics.Vulkan
} }
} }
public static ImageUsageFlags GetImageUsageFromFormat(GAL.Format format)
{
var usage = DefaultUsageFlags;
if (format.IsDepthOrStencil())
{
usage |= ImageUsageFlags.DepthStencilAttachmentBit;
}
else if (format.IsRtColorCompatible())
{
usage |= ImageUsageFlags.ColorAttachmentBit;
}
if (format.IsImageCompatible())
{
usage |= ImageUsageFlags.StorageBit;
}
return usage;
}
public static SampleCountFlags ConvertToSampleCountFlags(SampleCountFlags supportedSampleCounts, uint samples) public static SampleCountFlags ConvertToSampleCountFlags(SampleCountFlags supportedSampleCounts, uint samples)
{ {
if (samples == 0 || samples > (uint)SampleCountFlags.Count64Bit) if (samples == 0 || samples > (uint)SampleCountFlags.Count64Bit)

View File

@ -54,6 +54,7 @@ namespace Ryujinx.Graphics.Vulkan
gd.Textures.Add(this); gd.Textures.Add(this);
var format = _gd.FormatCapabilities.ConvertToVkFormat(info.Format); var format = _gd.FormatCapabilities.ConvertToVkFormat(info.Format);
var usage = TextureStorage.GetImageUsageFromFormat(info.Format);
var levels = (uint)info.Levels; var levels = (uint)info.Levels;
var layers = (uint)info.GetLayers(); var layers = (uint)info.GetLayers();
@ -94,7 +95,7 @@ namespace Ryujinx.Graphics.Vulkan
var subresourceRange = new ImageSubresourceRange(aspectFlags, (uint)firstLevel, levels, (uint)firstLayer, layers); var subresourceRange = new ImageSubresourceRange(aspectFlags, (uint)firstLevel, levels, (uint)firstLayer, layers);
var subresourceRangeDepth = new ImageSubresourceRange(aspectFlagsDepth, (uint)firstLevel, levels, (uint)firstLayer, layers); var subresourceRangeDepth = new ImageSubresourceRange(aspectFlagsDepth, (uint)firstLevel, levels, (uint)firstLayer, layers);
unsafe Auto<DisposableImageView> CreateImageView(ComponentMapping cm, ImageSubresourceRange sr, ImageViewType viewType, ImageUsageFlags usageFlags = 0) unsafe Auto<DisposableImageView> CreateImageView(ComponentMapping cm, ImageSubresourceRange sr, ImageViewType viewType, ImageUsageFlags usageFlags)
{ {
var usage = new ImageViewUsageCreateInfo() var usage = new ImageViewUsageCreateInfo()
{ {
@ -110,14 +111,14 @@ namespace Ryujinx.Graphics.Vulkan
Format = format, Format = format,
Components = cm, Components = cm,
SubresourceRange = sr, SubresourceRange = sr,
PNext = usageFlags == 0 ? null : &usage PNext = &usage
}; };
gd.Api.CreateImageView(device, imageCreateInfo, null, out var imageView).ThrowOnError(); gd.Api.CreateImageView(device, imageCreateInfo, null, out var imageView).ThrowOnError();
return new Auto<DisposableImageView>(new DisposableImageView(gd.Api, device, imageView), null, storage.GetImage()); return new Auto<DisposableImageView>(new DisposableImageView(gd.Api, device, imageView), null, storage.GetImage());
} }
_imageView = CreateImageView(componentMapping, subresourceRange, type); _imageView = CreateImageView(componentMapping, subresourceRange, type, ImageUsageFlags.SampledBit);
// Framebuffer attachments and storage images requires a identity component mapping. // Framebuffer attachments and storage images requires a identity component mapping.
var identityComponentMapping = new ComponentMapping( var identityComponentMapping = new ComponentMapping(
@ -126,7 +127,7 @@ namespace Ryujinx.Graphics.Vulkan
ComponentSwizzle.B, ComponentSwizzle.B,
ComponentSwizzle.A); ComponentSwizzle.A);
_imageViewIdentity = CreateImageView(identityComponentMapping, subresourceRangeDepth, type); _imageViewIdentity = CreateImageView(identityComponentMapping, subresourceRangeDepth, type, usage);
// Framebuffer attachments also require 3D textures to be bound as 2D array. // Framebuffer attachments also require 3D textures to be bound as 2D array.
if (info.Target == Target.Texture3D) if (info.Target == Target.Texture3D)
@ -144,7 +145,7 @@ namespace Ryujinx.Graphics.Vulkan
{ {
subresourceRange = new ImageSubresourceRange(aspectFlags, (uint)firstLevel, levels, (uint)firstLayer, (uint)info.Depth); subresourceRange = new ImageSubresourceRange(aspectFlags, (uint)firstLevel, levels, (uint)firstLayer, (uint)info.Depth);
_imageView2dArray = CreateImageView(identityComponentMapping, subresourceRange, ImageViewType.Type2DArray); _imageView2dArray = CreateImageView(identityComponentMapping, subresourceRange, ImageViewType.Type2DArray, usage);
} }
} }

View File

@ -1,5 +1,4 @@
using Ryujinx.HLE.HOS.Kernel.Common; using Ryujinx.HLE.HOS.Kernel.Memory;
using Ryujinx.HLE.HOS.Kernel.Memory;
using Ryujinx.HLE.HOS.Kernel.Process; using Ryujinx.HLE.HOS.Kernel.Process;
using Ryujinx.HLE.HOS.Kernel.Threading; using Ryujinx.HLE.HOS.Kernel.Threading;
using Ryujinx.Horizon.Common; using Ryujinx.Horizon.Common;
@ -71,4 +70,4 @@ namespace Ryujinx.HLE.HOS.Kernel
return null; return null;
} }
} }
} }

View File

@ -10,6 +10,24 @@ namespace Ryujinx.HLE.HOS.Services.Pm
{ {
public IDebugMonitorInterface(ServiceCtx context) { } public IDebugMonitorInterface(ServiceCtx context) { }
[CommandHipc(4)]
// GetProgramId() -> sf::Out<ncm::ProgramId> out_process_id
public ResultCode GetApplicationProcessId(ServiceCtx context)
{
// TODO: Not correct as it shouldn't be directly using kernel objects here
foreach (KProcess process in context.Device.System.KernelContext.Processes.Values)
{
if (process.IsApplication)
{
context.ResponseData.Write(process.Pid);
return ResultCode.Success;
}
}
return ResultCode.ProcessNotFound;
}
[CommandHipc(65000)] [CommandHipc(65000)]
// AtmosphereGetProcessInfo(os::ProcessId process_id) -> sf::OutCopyHandle out_process_handle, sf::Out<ncm::ProgramLocation> out_loc, sf::Out<cfg::OverrideStatus> out_status // AtmosphereGetProcessInfo(os::ProcessId process_id) -> sf::OutCopyHandle out_process_handle, sf::Out<ncm::ProgramLocation> out_loc, sf::Out<cfg::OverrideStatus> out_status
public ResultCode GetProcessInfo(ServiceCtx context) public ResultCode GetProcessInfo(ServiceCtx context)

View File

@ -1,8 +1,28 @@
namespace Ryujinx.HLE.HOS.Services.Pm using Ryujinx.HLE.HOS.Kernel;
using Ryujinx.HLE.HOS.Kernel.Process;
namespace Ryujinx.HLE.HOS.Services.Pm
{ {
[Service("pm:info")] [Service("pm:info")]
class IInformationInterface : IpcService class IInformationInterface : IpcService
{ {
public IInformationInterface(ServiceCtx context) { } public IInformationInterface(ServiceCtx context) { }
[CommandHipc(0)]
// GetProgramId(os::ProcessId process_id) -> sf::Out<ncm::ProgramId> out
public ResultCode GetProgramId(ServiceCtx context)
{
ulong pid = context.RequestData.ReadUInt64();
// TODO: Not correct as it shouldn't be directly using kernel objects here
if (context.Device.System.KernelContext.Processes.TryGetValue(pid, out KProcess process))
{
context.ResponseData.Write(process.TitleId);
return ResultCode.Success;
}
return ResultCode.ProcessNotFound;
}
} }
} }

View File

@ -0,0 +1,17 @@
namespace Ryujinx.HLE.HOS.Services.Pm
{
enum ResultCode
{
ModuleId = 15,
ErrorCodeShift = 9,
Success = 0,
ProcessNotFound = (1 << ErrorCodeShift) | ModuleId,
AlreadyStarted = (2 << ErrorCodeShift) | ModuleId,
NotTerminated = (3 << ErrorCodeShift) | ModuleId,
DebugHookInUse = (4 << ErrorCodeShift) | ModuleId,
ApplicationRunning = (5 << ErrorCodeShift) | ModuleId,
InvalidSize = (6 << ErrorCodeShift) | ModuleId,
}
}

View File

@ -38,9 +38,9 @@ namespace Ryujinx.Ui.App.Common
private readonly byte[] _nroIcon; private readonly byte[] _nroIcon;
private readonly byte[] _nsoIcon; private readonly byte[] _nsoIcon;
private VirtualFileSystem _virtualFileSystem; private readonly VirtualFileSystem _virtualFileSystem;
private Language _desiredTitleLanguage; private Language _desiredTitleLanguage;
private CancellationTokenSource _cancellationToken; private CancellationTokenSource _cancellationToken;
public ApplicationLibrary(VirtualFileSystem virtualFileSystem) public ApplicationLibrary(VirtualFileSystem virtualFileSystem)
{ {
@ -53,7 +53,7 @@ namespace Ryujinx.Ui.App.Common
_nsoIcon = GetResourceBytes("Ryujinx.Ui.Common.Resources.Icon_NSO.png"); _nsoIcon = GetResourceBytes("Ryujinx.Ui.Common.Resources.Icon_NSO.png");
} }
private byte[] GetResourceBytes(string resourceName) private static byte[] GetResourceBytes(string resourceName)
{ {
Stream resourceStream = Assembly.GetCallingAssembly().GetManifestResourceStream(resourceName); Stream resourceStream = Assembly.GetCallingAssembly().GetManifestResourceStream(resourceName);
byte[] resourceByteArray = new byte[resourceStream.Length]; byte[] resourceByteArray = new byte[resourceStream.Length];
@ -68,9 +68,9 @@ namespace Ryujinx.Ui.App.Common
_cancellationToken?.Cancel(); _cancellationToken?.Cancel();
} }
public void ReadControlData(IFileSystem controlFs, Span<byte> outProperty) public static void ReadControlData(IFileSystem controlFs, Span<byte> outProperty)
{ {
using var controlFile = new UniqueRef<IFile>(); using UniqueRef<IFile> controlFile = new();
controlFs.OpenFile(ref controlFile.Ref(), "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); controlFs.OpenFile(ref controlFile.Ref(), "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
controlFile.Get.Read(out _, 0, outProperty, ReadOption.None).ThrowIfFailure(); controlFile.Get.Read(out _, 0, outProperty, ReadOption.None).ThrowIfFailure();
@ -86,7 +86,7 @@ namespace Ryujinx.Ui.App.Common
_cancellationToken = new CancellationTokenSource(); _cancellationToken = new CancellationTokenSource();
// Builds the applications list with paths to found applications // Builds the applications list with paths to found applications
List<string> applications = new List<string>(); List<string> applications = new();
try try
{ {
@ -143,251 +143,246 @@ namespace Ryujinx.Ui.App.Common
string version = "0"; string version = "0";
byte[] applicationIcon = null; byte[] applicationIcon = null;
BlitStruct<ApplicationControlProperty> controlHolder = new BlitStruct<ApplicationControlProperty>(1); BlitStruct<ApplicationControlProperty> controlHolder = new(1);
try try
{ {
string extension = Path.GetExtension(applicationPath).ToLower(); string extension = Path.GetExtension(applicationPath).ToLower();
using (FileStream file = new FileStream(applicationPath, FileMode.Open, FileAccess.Read)) using FileStream file = new(applicationPath, FileMode.Open, FileAccess.Read);
if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci")
{ {
if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci") try
{ {
try PartitionFileSystem pfs;
bool isExeFs = false;
if (extension == ".xci")
{ {
PartitionFileSystem pfs; Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
bool isExeFs = false; pfs = xci.OpenPartition(XciPartitionType.Secure);
}
else
{
pfs = new PartitionFileSystem(file.AsStorage());
if (extension == ".xci") // If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application.
bool hasMainNca = false;
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*"))
{ {
Xci xci = new Xci(_virtualFileSystem.KeySet, file.AsStorage()); if (Path.GetExtension(fileEntry.FullPath).ToLower() == ".nca")
pfs = xci.OpenPartition(XciPartitionType.Secure);
}
else
{
pfs = new PartitionFileSystem(file.AsStorage());
// If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application.
bool hasMainNca = false;
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*"))
{ {
if (Path.GetExtension(fileEntry.FullPath).ToLower() == ".nca") using UniqueRef<IFile> ncaFile = new();
pfs.OpenFile(ref ncaFile.Ref(), fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage());
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
// Some main NCAs don't have a data partition, so check if the partition exists before opening it
if (nca.Header.ContentType == NcaContentType.Program && !(nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection()))
{ {
using var ncaFile = new UniqueRef<IFile>(); hasMainNca = true;
pfs.OpenFile(ref ncaFile.Ref(), fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); break;
Nca nca = new Nca(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage());
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
// Some main NCAs don't have a data partition, so check if the partition exists before opening it
if (nca.Header.ContentType == NcaContentType.Program && !(nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection()))
{
hasMainNca = true;
break;
}
}
else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main")
{
isExeFs = true;
} }
} }
else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main")
if (!hasMainNca && !isExeFs)
{ {
numApplicationsFound--; isExeFs = true;
continue;
} }
} }
if (isExeFs) if (!hasMainNca && !isExeFs)
{
applicationIcon = _nspIcon;
using var npdmFile = new UniqueRef<IFile>();
Result result = pfs.OpenFile(ref npdmFile.Ref(), "/main.npdm".ToU8Span(), OpenMode.Read);
if (ResultFs.PathNotFound.Includes(result))
{
Npdm npdm = new Npdm(npdmFile.Get.AsStream());
titleName = npdm.TitleName;
titleId = npdm.Aci0.TitleId.ToString("x16");
}
}
else
{
GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out titleId);
// Check if there is an update available.
if (IsUpdateApplied(titleId, out IFileSystem updatedControlFs))
{
// Replace the original ControlFs by the updated one.
controlFs = updatedControlFs;
}
ReadControlData(controlFs, controlHolder.ByteSpan);
GetGameInformation(ref controlHolder.Value, out titleName, out _, out developer, out version);
// Read the icon from the ControlFS and store it as a byte array
try
{
using var icon = new UniqueRef<IFile>();
controlFs.OpenFile(ref icon.Ref(), $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure();
using (MemoryStream stream = new MemoryStream())
{
icon.Get.AsStream().CopyTo(stream);
applicationIcon = stream.ToArray();
}
}
catch (HorizonResultException)
{
foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*"))
{
if (entry.Name == "control.nacp")
{
continue;
}
using var icon = new UniqueRef<IFile>();
controlFs.OpenFile(ref icon.Ref(), entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
using (MemoryStream stream = new MemoryStream())
{
icon.Get.AsStream().CopyTo(stream);
applicationIcon = stream.ToArray();
}
if (applicationIcon != null)
{
break;
}
}
if (applicationIcon == null)
{
applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon;
}
}
}
}
catch (MissingKeyException exception)
{
applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon;
Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}");
}
catch (InvalidDataException)
{
applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon;
Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {applicationPath}");
}
catch (Exception exception)
{
Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{applicationPath}' Error: {exception}");
numApplicationsFound--;
continue;
}
}
else if (extension == ".nro")
{
BinaryReader reader = new BinaryReader(file);
byte[] Read(long position, int size)
{
file.Seek(position, SeekOrigin.Begin);
return reader.ReadBytes(size);
}
try
{
file.Seek(24, SeekOrigin.Begin);
int assetOffset = reader.ReadInt32();
if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET")
{
byte[] iconSectionInfo = Read(assetOffset + 8, 0x10);
long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0);
long iconSize = BitConverter.ToInt64(iconSectionInfo, 8);
ulong nacpOffset = reader.ReadUInt64();
ulong nacpSize = reader.ReadUInt64();
// Reads and stores game icon as byte array
applicationIcon = Read(assetOffset + iconOffset, (int)iconSize);
// Read the NACP data
Read(assetOffset + (int)nacpOffset, (int)nacpSize).AsSpan().CopyTo(controlHolder.ByteSpan);
GetGameInformation(ref controlHolder.Value, out titleName, out titleId, out developer, out version);
}
else
{
applicationIcon = _nroIcon;
titleName = Path.GetFileNameWithoutExtension(applicationPath);
}
}
catch
{
Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}");
numApplicationsFound--;
continue;
}
}
else if (extension == ".nca")
{
try
{
Nca nca = new Nca(_virtualFileSystem.KeySet, new FileStream(applicationPath, FileMode.Open, FileAccess.Read).AsStorage());
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
if (nca.Header.ContentType != NcaContentType.Program || (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection()))
{ {
numApplicationsFound--; numApplicationsFound--;
continue; continue;
} }
} }
catch (InvalidDataException)
{
Logger.Warning?.Print(LogClass.Application, $"The NCA header content type check has failed. This is usually because the header key is incorrect or missing. Errored File: {applicationPath}");
}
catch
{
Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}");
if (isExeFs)
{
applicationIcon = _nspIcon;
using UniqueRef<IFile> npdmFile = new();
Result result = pfs.OpenFile(ref npdmFile.Ref(), "/main.npdm".ToU8Span(), OpenMode.Read);
if (ResultFs.PathNotFound.Includes(result))
{
Npdm npdm = new(npdmFile.Get.AsStream());
titleName = npdm.TitleName;
titleId = npdm.Aci0.TitleId.ToString("x16");
}
}
else
{
GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out titleId);
// Check if there is an update available.
if (IsUpdateApplied(titleId, out IFileSystem updatedControlFs))
{
// Replace the original ControlFs by the updated one.
controlFs = updatedControlFs;
}
ReadControlData(controlFs, controlHolder.ByteSpan);
GetGameInformation(ref controlHolder.Value, out titleName, out _, out developer, out version);
// Read the icon from the ControlFS and store it as a byte array
try
{
using UniqueRef<IFile> icon = new();
controlFs.OpenFile(ref icon.Ref(), $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure();
using MemoryStream stream = new();
icon.Get.AsStream().CopyTo(stream);
applicationIcon = stream.ToArray();
}
catch (HorizonResultException)
{
foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*"))
{
if (entry.Name == "control.nacp")
{
continue;
}
using var icon = new UniqueRef<IFile>();
controlFs.OpenFile(ref icon.Ref(), entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
using MemoryStream stream = new();
icon.Get.AsStream().CopyTo(stream);
applicationIcon = stream.ToArray();
if (applicationIcon != null)
{
break;
}
}
applicationIcon ??= extension == ".xci" ? _xciIcon : _nspIcon;
}
}
}
catch (MissingKeyException exception)
{
applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon;
Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}");
}
catch (InvalidDataException)
{
applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon;
Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {applicationPath}");
}
catch (Exception exception)
{
Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{applicationPath}' Error: {exception}");
numApplicationsFound--;
continue;
}
}
else if (extension == ".nro")
{
BinaryReader reader = new(file);
byte[] Read(long position, int size)
{
file.Seek(position, SeekOrigin.Begin);
return reader.ReadBytes(size);
}
try
{
file.Seek(24, SeekOrigin.Begin);
int assetOffset = reader.ReadInt32();
if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET")
{
byte[] iconSectionInfo = Read(assetOffset + 8, 0x10);
long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0);
long iconSize = BitConverter.ToInt64(iconSectionInfo, 8);
ulong nacpOffset = reader.ReadUInt64();
ulong nacpSize = reader.ReadUInt64();
// Reads and stores game icon as byte array
applicationIcon = Read(assetOffset + iconOffset, (int)iconSize);
// Read the NACP data
Read(assetOffset + (int)nacpOffset, (int)nacpSize).AsSpan().CopyTo(controlHolder.ByteSpan);
GetGameInformation(ref controlHolder.Value, out titleName, out titleId, out developer, out version);
}
else
{
applicationIcon = _nroIcon;
titleName = Path.GetFileNameWithoutExtension(applicationPath);
}
}
catch
{
Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}");
numApplicationsFound--;
continue;
}
}
else if (extension == ".nca")
{
try
{
Nca nca = new(_virtualFileSystem.KeySet, new FileStream(applicationPath, FileMode.Open, FileAccess.Read).AsStorage());
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
if (nca.Header.ContentType != NcaContentType.Program || (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection()))
{
numApplicationsFound--; numApplicationsFound--;
continue; continue;
} }
applicationIcon = _ncaIcon;
titleName = Path.GetFileNameWithoutExtension(applicationPath);
} }
// If its an NSO we just set defaults catch (InvalidDataException)
else if (extension == ".nso")
{ {
applicationIcon = _nsoIcon; Logger.Warning?.Print(LogClass.Application, $"The NCA header content type check has failed. This is usually because the header key is incorrect or missing. Errored File: {applicationPath}");
titleName = Path.GetFileNameWithoutExtension(applicationPath);
} }
catch
{
Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}");
numApplicationsFound--;
continue;
}
applicationIcon = _ncaIcon;
titleName = Path.GetFileNameWithoutExtension(applicationPath);
}
// If its an NSO we just set defaults
else if (extension == ".nso")
{
applicationIcon = _nsoIcon;
titleName = Path.GetFileNameWithoutExtension(applicationPath);
} }
} }
catch (IOException exception) catch (IOException exception)
@ -404,14 +399,21 @@ namespace Ryujinx.Ui.App.Common
appMetadata.Title = titleName; appMetadata.Title = titleName;
}); });
if (appMetadata.LastPlayed != "Never" && !DateTime.TryParse(appMetadata.LastPlayed, out _)) if (appMetadata.LastPlayed != "Never")
{ {
Logger.Warning?.Print(LogClass.Application, $"Last played datetime \"{appMetadata.LastPlayed}\" is invalid for current system culture, skipping (did current culture change?)"); if (!DateTime.TryParse(appMetadata.LastPlayed, out _))
{
Logger.Warning?.Print(LogClass.Application, $"Last played datetime \"{appMetadata.LastPlayed}\" is invalid for current system culture, skipping (did current culture change?)");
appMetadata.LastPlayed = "Never"; appMetadata.LastPlayed = "Never";
}
else
{
appMetadata.LastPlayed = appMetadata.LastPlayed[..^3];
}
} }
ApplicationData data = new ApplicationData ApplicationData data = new()
{ {
Favorite = appMetadata.Favorite, Favorite = appMetadata.Favorite,
Icon = applicationIcon, Icon = applicationIcon,
@ -419,7 +421,7 @@ namespace Ryujinx.Ui.App.Common
TitleId = titleId, TitleId = titleId,
Developer = developer, Developer = developer,
Version = version, Version = version,
TimePlayed = ConvertSecondsToReadableString(appMetadata.TimePlayed), TimePlayed = ConvertSecondsToFormattedString(appMetadata.TimePlayed),
TimePlayedNum = appMetadata.TimePlayed, TimePlayedNum = appMetadata.TimePlayed,
LastPlayed = appMetadata.LastPlayed, LastPlayed = appMetadata.LastPlayed,
FileExtension = Path.GetExtension(applicationPath).ToUpper().Remove(0, 1), FileExtension = Path.GetExtension(applicationPath).ToUpper().Remove(0, 1),
@ -488,10 +490,9 @@ namespace Ryujinx.Ui.App.Common
appMetadata = new ApplicationMetadata(); appMetadata = new ApplicationMetadata();
using (FileStream stream = File.Create(metadataFile, 4096, FileOptions.WriteThrough)) using FileStream stream = File.Create(metadataFile, 4096, FileOptions.WriteThrough);
{
JsonHelper.Serialize(stream, appMetadata, true); JsonHelper.Serialize(stream, appMetadata, true);
}
} }
try try
@ -509,10 +510,9 @@ namespace Ryujinx.Ui.App.Common
{ {
modifyFunction(appMetadata); modifyFunction(appMetadata);
using (FileStream stream = File.Create(metadataFile, 4096, FileOptions.WriteThrough)) using FileStream stream = File.Create(metadataFile, 4096, FileOptions.WriteThrough);
{
JsonHelper.Serialize(stream, appMetadata, true); JsonHelper.Serialize(stream, appMetadata, true);
}
} }
return appMetadata; return appMetadata;
@ -529,192 +529,177 @@ namespace Ryujinx.Ui.App.Common
{ {
string extension = Path.GetExtension(applicationPath).ToLower(); string extension = Path.GetExtension(applicationPath).ToLower();
using (FileStream file = new FileStream(applicationPath, FileMode.Open, FileAccess.Read)) using FileStream file = new(applicationPath, FileMode.Open, FileAccess.Read);
if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci")
{ {
if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci") try
{ {
try PartitionFileSystem pfs;
bool isExeFs = false;
if (extension == ".xci")
{ {
PartitionFileSystem pfs; Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
bool isExeFs = false; pfs = xci.OpenPartition(XciPartitionType.Secure);
}
else
{
pfs = new PartitionFileSystem(file.AsStorage());
if (extension == ".xci") foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*"))
{ {
Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage()); if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main")
pfs = xci.OpenPartition(XciPartitionType.Secure);
}
else
{
pfs = new PartitionFileSystem(file.AsStorage());
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*"))
{ {
if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main") isExeFs = true;
{
isExeFs = true;
}
} }
} }
}
if (isExeFs) if (isExeFs)
{
applicationIcon = _nspIcon;
}
else
{
// Store the ControlFS in variable called controlFs
GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out _);
// Read the icon from the ControlFS and store it as a byte array
try
{ {
applicationIcon = _nspIcon; using var icon = new UniqueRef<IFile>();
controlFs.OpenFile(ref icon.Ref(), $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure();
using MemoryStream stream = new();
icon.Get.AsStream().CopyTo(stream);
applicationIcon = stream.ToArray();
} }
else catch (HorizonResultException)
{ {
// Store the ControlFS in variable called controlFs foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*"))
GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out _);
// Read the icon from the ControlFS and store it as a byte array
try
{ {
if (entry.Name == "control.nacp")
{
continue;
}
using var icon = new UniqueRef<IFile>(); using var icon = new UniqueRef<IFile>();
controlFs.OpenFile(ref icon.Ref(), $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure(); controlFs.OpenFile(ref icon.Ref(), entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
using (MemoryStream stream = new MemoryStream()) using (MemoryStream stream = new())
{ {
icon.Get.AsStream().CopyTo(stream); icon.Get.AsStream().CopyTo(stream);
applicationIcon = stream.ToArray(); applicationIcon = stream.ToArray();
} }
}
catch (HorizonResultException) if (applicationIcon != null)
{
foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*"))
{ {
if (entry.Name == "control.nacp") break;
{
continue;
}
using var icon = new UniqueRef<IFile>();
controlFs.OpenFile(ref icon.Ref(), entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
using (MemoryStream stream = new MemoryStream())
{
icon.Get.AsStream().CopyTo(stream);
applicationIcon = stream.ToArray();
}
if (applicationIcon != null)
{
break;
}
}
if (applicationIcon == null)
{
applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon;
} }
} }
applicationIcon ??= extension == ".xci" ? _xciIcon : _nspIcon;
} }
} }
catch (MissingKeyException)
{
applicationIcon = extension == ".xci"
? _xciIcon
: _nspIcon;
}
catch (InvalidDataException)
{
applicationIcon = extension == ".xci"
? _xciIcon
: _nspIcon;
}
catch (Exception exception)
{
Logger.Warning?.Print(LogClass.Application,
$"The file encountered was not of a valid type. File: '{applicationPath}' Error: {exception}");
}
} }
else if (extension == ".nro") catch (MissingKeyException)
{ {
BinaryReader reader = new(file); applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon;
byte[] Read(long position, int size)
{
file.Seek(position, SeekOrigin.Begin);
return reader.ReadBytes(size);
}
try
{
file.Seek(24, SeekOrigin.Begin);
int assetOffset = reader.ReadInt32();
if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET")
{
byte[] iconSectionInfo = Read(assetOffset + 8, 0x10);
long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0);
long iconSize = BitConverter.ToInt64(iconSectionInfo, 8);
// Reads and stores game icon as byte array
applicationIcon = Read(assetOffset + iconOffset, (int)iconSize);
}
else
{
applicationIcon = _nroIcon;
}
}
catch
{
Logger.Warning?.Print(LogClass.Application,
$"The file encountered was not of a valid type. Errored File: {applicationPath}");
}
} }
else if (extension == ".nca") catch (InvalidDataException)
{ {
applicationIcon = _ncaIcon; applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon;
} }
// If its an NSO we just set defaults catch (Exception exception)
else if (extension == ".nso")
{ {
applicationIcon = _nsoIcon; Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{applicationPath}' Error: {exception}");
} }
} }
else if (extension == ".nro")
{
BinaryReader reader = new(file);
byte[] Read(long position, int size)
{
file.Seek(position, SeekOrigin.Begin);
return reader.ReadBytes(size);
}
try
{
file.Seek(24, SeekOrigin.Begin);
int assetOffset = reader.ReadInt32();
if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET")
{
byte[] iconSectionInfo = Read(assetOffset + 8, 0x10);
long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0);
long iconSize = BitConverter.ToInt64(iconSectionInfo, 8);
// Reads and stores game icon as byte array
applicationIcon = Read(assetOffset + iconOffset, (int)iconSize);
}
else
{
applicationIcon = _nroIcon;
}
}
catch
{
Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}");
}
}
else if (extension == ".nca")
{
applicationIcon = _ncaIcon;
}
// If its an NSO we just set defaults
else if (extension == ".nso")
{
applicationIcon = _nsoIcon;
}
} }
} }
catch(Exception) catch(Exception)
{ {
Logger.Warning?.Print(LogClass.Application, Logger.Warning?.Print(LogClass.Application, $"Could not retrieve a valid icon for the app. Default icon will be used. Errored File: {applicationPath}");
$"Could not retrieve a valid icon for the app. Default icon will be used. Errored File: {applicationPath}");
} }
return applicationIcon ?? _ncaIcon; return applicationIcon ?? _ncaIcon;
} }
private string ConvertSecondsToReadableString(double seconds) private static string ConvertSecondsToFormattedString(double seconds)
{ {
const int secondsPerMinute = 60; System.TimeSpan time = System.TimeSpan.FromSeconds(seconds);
const int secondsPerHour = secondsPerMinute * 60;
const int secondsPerDay = secondsPerHour * 24;
string readableString; string timeString;
if (time.Days != 0)
if (seconds < secondsPerMinute)
{ {
readableString = $"{seconds} seconds"; timeString = $"{time.Days}d {time.Hours:D2}h {time.Minutes:D2}m";
} }
else if (seconds < secondsPerHour) else if (time.Hours != 0)
{ {
readableString = $"{Math.Round(seconds / secondsPerMinute, 0, MidpointRounding.AwayFromZero)} minutes"; timeString = $"{time.Hours:D2}h {time.Minutes:D2}m";
} }
else if (seconds < secondsPerDay) else if (time.Minutes != 0)
{ {
readableString = $"{Math.Round(seconds / secondsPerHour, 1, MidpointRounding.AwayFromZero)} hours"; timeString = $"{time.Minutes:D2}m";
} }
else else
{ {
readableString = $"{Math.Round(seconds / secondsPerDay, 1, MidpointRounding.AwayFromZero)} days"; timeString = "Never";
} }
return readableString; return timeString;
} }
private void GetGameInformation(ref ApplicationControlProperty controlData, out string titleName, out string titleId, out string publisher, out string version) private void GetGameInformation(ref ApplicationControlProperty controlData, out string titleName, out string titleId, out string publisher, out string version)
@ -797,8 +782,7 @@ namespace Ryujinx.Ui.App.Common
} }
catch (InvalidDataException) catch (InvalidDataException)
{ {
Logger.Warning?.Print(LogClass.Application, Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {updatePath}");
$"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {updatePath}");
} }
catch (MissingKeyException exception) catch (MissingKeyException exception)
{ {

View File

@ -1,19 +1,75 @@
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
namespace Ryujinx.Ui.Common.Helper namespace Ryujinx.Ui.Common.Helper
{ {
public static class OpenHelper public static partial class OpenHelper
{ {
[LibraryImport("shell32.dll", SetLastError = true)]
public static partial int SHOpenFolderAndSelectItems(IntPtr pidlFolder, uint cidl, IntPtr apidl, uint dwFlags);
[LibraryImport("shell32.dll", SetLastError = true)]
public static partial void ILFree(IntPtr pidlList);
[LibraryImport("shell32.dll", SetLastError = true)]
public static partial IntPtr ILCreateFromPathW([MarshalAs(UnmanagedType.LPWStr)] string pszPath);
public static void OpenFolder(string path) public static void OpenFolder(string path)
{ {
Process.Start(new ProcessStartInfo if (Directory.Exists(path))
{ {
FileName = path, Process.Start(new ProcessStartInfo
UseShellExecute = true, {
Verb = "open" FileName = path,
}); UseShellExecute = true,
Verb = "open"
});
}
else
{
Logger.Notice.Print(LogClass.Application, $"Directory \"{path}\" doesn't exist!");
}
}
public static void LocateFile(string path)
{
if (File.Exists(path))
{
if (OperatingSystem.IsWindows())
{
IntPtr pidlList = ILCreateFromPathW(path);
if (pidlList != IntPtr.Zero)
{
try
{
Marshal.ThrowExceptionForHR(SHOpenFolderAndSelectItems(pidlList, 0, IntPtr.Zero, 0));
}
finally
{
ILFree(pidlList);
}
}
}
else if (OperatingSystem.IsMacOS())
{
Process.Start("open", $"-R \"{path}\"");
}
else if (OperatingSystem.IsLinux())
{
Process.Start("dbus-send", $"--session --print-reply --dest=org.freedesktop.FileManager1 --type=method_call /org/freedesktop/FileManager1 org.freedesktop.FileManager1.ShowItems array:string:\"file://{path}\" string:\"\"");
}
else
{
OpenFolder(Path.GetDirectoryName(path));
}
}
else
{
Logger.Notice.Print(LogClass.Application, $"File \"{path}\" doesn't exist!");
}
} }
public static void OpenUrl(string url) public static void OpenUrl(string url)

View File

@ -7,65 +7,33 @@ namespace Ryujinx.Ui.Helper
{ {
public static int TimePlayedSort(ITreeModel model, TreeIter a, TreeIter b) public static int TimePlayedSort(ITreeModel model, TreeIter a, TreeIter b)
{ {
string aValue = model.GetValue(a, 5).ToString(); static string ReverseFormat(string time)
string bValue = model.GetValue(b, 5).ToString(); {
float aFloat; if (time == "Never")
float bFloat; {
return "00";
}
if (aValue.Length > 7 && aValue[^7..] == "minutes") var numbers = time.Split(new char[] { 'd', 'h', 'm' });
{
aValue = aValue.Replace("minutes", ""); time = time.Replace(" ", "").Replace("d", ".").Replace("h", ":").Replace("m", "");
aFloat = (float.Parse(aValue) * 60);
} if (numbers.Length == 2)
else if (aValue.Length > 5 && aValue[^5..] == "hours") {
{ return $"00.00:{time}";
aValue = aValue.Replace("hours", ""); }
aFloat = (float.Parse(aValue) * 3600); else if (numbers.Length == 3)
} {
else if (aValue.Length > 4 && aValue[^4..] == "days") return $"00.{time}";
{ }
aValue = aValue.Replace("days", "");
aFloat = (float.Parse(aValue) * 86400); return time;
}
else
{
aValue = aValue.Replace("seconds", "");
aFloat = float.Parse(aValue);
} }
if (bValue.Length > 7 && bValue[^7..] == "minutes") string aValue = ReverseFormat(model.GetValue(a, 5).ToString());
{ string bValue = ReverseFormat(model.GetValue(b, 5).ToString());
bValue = bValue.Replace("minutes", "");
bFloat = (float.Parse(bValue) * 60);
}
else if (bValue.Length > 5 && bValue[^5..] == "hours")
{
bValue = bValue.Replace("hours", "");
bFloat = (float.Parse(bValue) * 3600);
}
else if (bValue.Length > 4 && bValue[^4..] == "days")
{
bValue = bValue.Replace("days", "");
bFloat = (float.Parse(bValue) * 86400);
}
else
{
bValue = bValue[0..^8];
bFloat = float.Parse(bValue);
}
if (aFloat > bFloat) return TimeSpan.Compare(TimeSpan.Parse(aValue), TimeSpan.Parse(bValue));
{
return -1;
}
else if (bFloat > aFloat)
{
return 1;
}
else
{
return 0;
}
} }
public static int LastPlayedSort(ITreeModel model, TreeIter a, TreeIter b) public static int LastPlayedSort(ITreeModel model, TreeIter a, TreeIter b)
@ -123,4 +91,4 @@ namespace Ryujinx.Ui.Helper
} }
} }
} }
} }