Compare commits

...

8 Commits

Author SHA1 Message Date
f0e27a23a5 Add short duration texture cache (#3754)
* Add short duration texture cache

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

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

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

* Small optimization

* Don't forget this.

* Add short cache dictionary

* Address feedback

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

* ava: Minor Renderer cleanup

* Don't supress potential NRE warning

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

Co-authored-by: Ac_K <Acoustik666@gmail.com>
2023-01-16 03:59:41 +01:00
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
38 changed files with 1576 additions and 1310 deletions

View File

@ -12,9 +12,9 @@ using Ryujinx.Audio.Integration;
using Ryujinx.Ava.Common;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Input;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.Renderer;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common;
@ -44,7 +44,6 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop;
@ -66,8 +65,8 @@ namespace Ryujinx.Ava
private const float VolumeDelta = 0.05f;
private static readonly Cursor InvisibleCursor = new(StandardCursorType.None);
private readonly IntPtr InvisibleCursorWin;
private readonly IntPtr DefaultCursorWin;
private readonly IntPtr InvisibleCursorWin;
private readonly IntPtr DefaultCursorWin;
private readonly long _ticksPerFrame;
private readonly Stopwatch _chrono;
@ -80,6 +79,7 @@ namespace Ryujinx.Ava
private readonly MainWindowViewModel _viewModel;
private readonly IKeyboard _keyboardInterface;
private readonly TopLevel _topLevel;
public RendererHost _rendererHost;
private readonly GraphicsDebugLevel _glLogLevel;
private float _newVolume;
@ -105,7 +105,6 @@ namespace Ryujinx.Ava
public event EventHandler AppExit;
public event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent;
public RendererHost Renderer { get; }
public VirtualFileSystem VirtualFileSystem { get; }
public ContentManager ContentManager { get; }
public NpadManager NpadManager { get; }
@ -117,7 +116,6 @@ namespace Ryujinx.Ava
public string ApplicationPath { get; private set; }
public bool ScreenshotRequested { get; set; }
public AppHost(
RendererHost renderer,
InputManager inputManager,
@ -144,11 +142,12 @@ namespace Ryujinx.Ava
NpadManager = _inputManager.CreateNpadManager();
TouchScreenManager = _inputManager.CreateTouchScreenManager();
Renderer = renderer;
ApplicationPath = applicationPath;
VirtualFileSystem = virtualFileSystem;
ContentManager = contentManager;
_rendererHost = renderer;
_chrono = new Stopwatch();
_ticksPerFrame = Stopwatch.Frequency / TargetFps;
@ -183,10 +182,10 @@ namespace Ryujinx.Ava
{
_lastCursorMoveTime = Stopwatch.GetTimestamp();
if ((Renderer.Content as EmbeddedWindow).TransformedBounds != null)
if (_rendererHost.EmbeddedWindow.TransformedBounds != null)
{
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 &&
point.X <= bounds.Width + bounds.X &&
@ -318,7 +317,7 @@ namespace Ryujinx.Ava
_viewModel.SetUIProgressHandlers(Device);
Renderer.SizeChanged += Window_SizeChanged;
_rendererHost.SizeChanged += Window_SizeChanged;
_isActive = true;
@ -430,11 +429,11 @@ namespace Ryujinx.Ava
_windowsMultimediaTimerResolution = null;
}
Renderer?.MakeCurrent();
(_rendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.MakeCurrent();
Device.DisposeGpu();
Renderer?.MakeCurrent(null);
(_rendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.MakeCurrent(null);
}
private void HideCursorState_Changed(object sender, ReactiveEventArgs<bool> state)
@ -635,11 +634,12 @@ namespace Ryujinx.Ava
// Initialize Renderer.
IRenderer renderer;
if (Renderer.IsVulkan)
if (ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.Vulkan)
{
string preferredGpu = ConfigurationState.Instance.Graphics.PreferredGpu.Value;
renderer = new VulkanRenderer(Renderer.CreateVulkanSurface, VulkanHelper.GetRequiredInstanceExtensions, preferredGpu);
renderer = new VulkanRenderer(
(_rendererHost.EmbeddedWindow as EmbeddedWindowVulkan).CreateSurface,
VulkanHelper.GetRequiredInstanceExtensions,
ConfigurationState.Instance.Graphics.PreferredGpu.Value);
}
else
{
@ -787,14 +787,12 @@ namespace Ryujinx.Ava
_renderer.ScreenCaptured += Renderer_ScreenCaptured;
(_renderer as OpenGLRenderer)?.InitializeBackgroundContext(SPBOpenGLContext.CreateBackgroundContext(Renderer.GetContext()));
Renderer.MakeCurrent();
(_rendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.InitializeBackgroundContext(_renderer);
Device.Gpu.Renderer.Initialize(_glLogLevel);
Width = (int)Renderer.Bounds.Width;
Height = (int)Renderer.Bounds.Height;
Width = (int)_rendererHost.Bounds.Width;
Height = (int)_rendererHost.Bounds.Height;
_renderer.Window.SetSize((int)(Width * _topLevel.PlatformImpl.RenderScaling), (int)(Height * _topLevel.PlatformImpl.RenderScaling));
@ -827,7 +825,7 @@ namespace Ryujinx.Ava
_viewModel.SwitchToRenderer(false);
}
Device.PresentFrame(() => Renderer?.SwapBuffers());
Device.PresentFrame(() => (_rendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.SwapBuffers());
}
if (_ticks >= _ticksPerFrame)
@ -837,7 +835,7 @@ namespace Ryujinx.Ava
}
});
Renderer?.MakeCurrent(null);
(_rendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.MakeCurrent(null);
}
public void UpdateStatus()
@ -853,7 +851,7 @@ namespace Ryujinx.Ava
StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs(
Device.EnableDeviceVsync,
LocaleManager.Instance[LocaleKeys.VolumeShort] + $": {(int)(Device.GetVolume() * 100)}%",
Renderer.IsVulkan ? "Vulkan" : "OpenGL",
ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.Vulkan ? "Vulkan" : "OpenGL",
dockedMode,
ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(),
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!",
"OpenSetupGuideMessage": "Open the Setup Guide",
"NoUpdate": "No Update",
"TitleUpdateVersionLabel": "Version {0} - {1}",
"TitleUpdateVersionLabel": "Version {0}",
"RyujinxInfo": "Ryujinx - Info",
"RyujinxConfirm": "Ryujinx - Confirmation",
"FileDialogAllTypes": "All types",
@ -585,7 +585,7 @@
"UserProfilesSetProfileImage": "Set Profile Image",
"UserProfileEmptyNameError": "Name is required",
"UserProfileNoImageError": "Profile image must be set",
"GameUpdateWindowHeading": "{0} Update(s) available for {1} ({2})",
"GameUpdateWindowHeading": "Manage Updates for {0} ({1})",
"SettingsTabHotkeysResScaleUpHotkey": "Increase resolution:",
"SettingsTabHotkeysResScaleDownHotkey": "Decrease resolution:",
"UserProfilesName": "Name:",

View File

@ -136,7 +136,7 @@ namespace Ryujinx.Ava.UI.Applet
Dispatcher.UIThread.Post(() =>
{
_hiddenTextBox.Clear();
_parent.ViewModel.RendererControl.Focus();
_parent.ViewModel.RendererHostControl.Focus();
_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
{
internal class TitleUpdateModel
public class TitleUpdateModel
{
public bool IsEnabled { get; set; }
public bool IsNoUpdate { get; }
public ApplicationControlProperty Control { get; }
public string Path { get; }
public string Label => IsNoUpdate
? LocaleManager.Instance[LocaleKeys.NoUpdate]
: string.Format(LocaleManager.Instance[LocaleKeys.TitleUpdateVersionLabel], Control.DisplayVersionString.ToString(),
Path);
public string Label => string.Format(LocaleManager.Instance[LocaleKeys.TitleUpdateVersionLabel], Control.DisplayVersionString.ToString());
public TitleUpdateModel(ApplicationControlProperty control, string path, bool isNoUpdate = false)
public TitleUpdateModel(ApplicationControlProperty control, string path)
{
Control = control;
Path = path;
IsNoUpdate = isNoUpdate;
}
}
}

View File

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

View File

@ -1,5 +1,8 @@
using OpenTK.Graphics.OpenGL;
using Ryujinx.Common.Configuration;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.OpenGL;
using Ryujinx.Ui.Common.Configuration;
using SPB.Graphics;
using SPB.Graphics.OpenGL;
using SPB.Platform;
@ -7,26 +10,20 @@ using SPB.Platform.WGL;
using SPB.Windowing;
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;
public OpenGLContextBase Context { get; set; }
public OpenGLEmbeddedWindow(int major, int minor, GraphicsDebugLevel graphicsDebugLevel)
{
_major = major;
_minor = minor;
_graphicsDebugLevel = graphicsDebugLevel;
}
public EmbeddedWindowOpenGL() { }
protected override void OnWindowDestroying()
{
Context.Dispose();
base.OnWindowDestroying();
}
@ -48,19 +45,20 @@ namespace Ryujinx.Ava.UI.Helpers
}
var flags = OpenGLContextFlags.Compat;
if (_graphicsDebugLevel != GraphicsDebugLevel.None)
if (ConfigurationState.Instance.Logger.GraphicsDebugLevel != GraphicsDebugLevel.None)
{
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.MakeCurrent(_window);
var bindingsContext = new OpenToolkitBindingsContext(Context.GetProcAddress);
GL.LoadBindings(new OpenTKBindingsContext(Context.GetProcAddress));
GL.LoadBindings(bindingsContext);
Context.MakeCurrent(null);
}
@ -76,7 +74,14 @@ namespace Ryujinx.Ava.UI.Helpers
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 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;
public OpenToolkitBindingsContext(Func<string, IntPtr> getProcAddress)
public OpenTKBindingsContext(Func<string, IntPtr> getProcAddress)
{
_getProcAddress = getProcAddress;
}

View File

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

View File

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

View File

@ -5,17 +5,17 @@ using SPB.Graphics.OpenGL;
using SPB.Platform;
using SPB.Windowing;
namespace Ryujinx.Ava.UI.Helpers
namespace Ryujinx.Ava.UI.Renderer
{
class SPBOpenGLContext : IOpenGLContext
{
private OpenGLContextBase _context;
private NativeWindowBase _window;
private readonly OpenGLContextBase _context;
private readonly NativeWindowBase _window;
private SPBOpenGLContext(OpenGLContextBase context, NativeWindowBase window)
{
_context = context;
_window = window;
_window = window;
}
public void Dispose()
@ -32,12 +32,12 @@ namespace Ryujinx.Ava.UI.Helpers
public static SPBOpenGLContext CreateBackgroundContext(OpenGLContextBase 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.MakeCurrent(window);
GL.LoadBindings(new OpenToolkitBindingsContext(context.GetProcAddress));
GL.LoadBindings(new OpenTKBindingsContext(context.GetProcAddress));
context.MakeCurrent(null);

View File

@ -13,6 +13,7 @@ using Ryujinx.Ava.Input;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.Renderer;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
@ -870,7 +871,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public Action<bool> SwitchToGameControl { get; private set; }
public Action<Control> SetMainContent { 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 LibHacHorizonManager LibHacHorizonManager { get; internal set; }
public IHostUiHandler UiHandler { get; internal set; }
@ -1144,7 +1145,7 @@ namespace Ryujinx.Ava.UI.ViewModels
private void InitializeGame()
{
RendererControl.RendererInitialized += GlRenderer_Created;
RendererHostControl.WindowCreated += RendererHost_Created;
AppHost.StatusUpdatedEvent += Update_StatusBar;
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);
@ -1601,13 +1602,9 @@ namespace Ryujinx.Ava.UI.ViewModels
public async void OpenTitleUpdateManager()
{
ApplicationData selection = SelectedApplication;
if (selection != null)
if (SelectedApplication != null)
{
if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
await new TitleUpdateWindow(VirtualFileSystem, ulong.Parse(selection.TitleId, NumberStyles.HexNumber), selection.TitleName).ShowDialog(desktop.MainWindow);
}
await TitleUpdateWindow.Show(VirtualFileSystem, ulong.Parse(SelectedApplication.TitleId, NumberStyles.HexNumber), SelectedApplication.TitleName);
}
}
@ -1735,18 +1732,10 @@ namespace Ryujinx.Ava.UI.ViewModels
PrepareLoadScreen();
RendererControl = new RendererHost(ConfigurationState.Instance.Logger.GraphicsDebugLevel);
if (ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.OpenGl)
{
RendererControl.CreateOpenGL();
}
else
{
RendererControl.CreateVulkan();
}
RendererHostControl = new RendererHost();
AppHost = new AppHost(
RendererControl,
RendererHostControl,
InputManager,
path,
VirtualFileSystem,
@ -1787,9 +1776,9 @@ namespace Ryujinx.Ava.UI.ViewModels
{
SwitchToGameControl(startFullscreen);
SetMainContent(RendererControl);
SetMainContent(RendererHostControl);
RendererControl.Focus();
RendererHostControl.Focus();
});
}
@ -1857,8 +1846,8 @@ namespace Ryujinx.Ava.UI.ViewModels
HandleRelaunch();
});
RendererControl.RendererInitialized -= GlRenderer_Created;
RendererControl = null;
RendererHostControl.WindowCreated -= RendererHost_Created;
RendererHostControl = 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">
<ListBox
Name="SaveList"
VirtualizationMode="None"
Items="{Binding Views}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
@ -116,6 +117,9 @@
<Setter Property="Margin" Value="5" />
<Setter Property="CornerRadius" Value="4" />
</Style>
<Style Selector="ListBoxItem:selected /template/ Border#SelectionIndicator">
<Setter Property="IsVisible" Value="False" />
</Style>
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate x:DataType="models:SaveModel">

View File

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

View File

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

@ -4,6 +4,28 @@ using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Image
{
/// <summary>
/// An entry on the short duration texture cache.
/// </summary>
class ShortTextureCacheEntry
{
public readonly TextureDescriptor Descriptor;
public readonly int InvalidatedSequence;
public readonly Texture Texture;
/// <summary>
/// Create a new entry on the short duration texture cache.
/// </summary>
/// <param name="descriptor">Last descriptor that referenced the texture</param>
/// <param name="texture">The texture</param>
public ShortTextureCacheEntry(TextureDescriptor descriptor, Texture texture)
{
Descriptor = descriptor;
InvalidatedSequence = texture.InvalidatedSequence;
Texture = texture;
}
}
/// <summary>
/// A texture cache that automatically removes older textures that are not used for some time.
/// The cache works with a rotated list with a fixed size. When new textures are added, the
@ -16,6 +38,11 @@ namespace Ryujinx.Graphics.Gpu.Image
private readonly LinkedList<Texture> _textures;
private readonly ConcurrentQueue<Texture> _deferredRemovals;
private HashSet<ShortTextureCacheEntry> _shortCacheBuilder;
private HashSet<ShortTextureCacheEntry> _shortCache;
private Dictionary<TextureDescriptor, ShortTextureCacheEntry> _shortCacheLookup;
/// <summary>
/// Creates a new instance of the automatic deletion cache.
/// </summary>
@ -23,6 +50,11 @@ namespace Ryujinx.Graphics.Gpu.Image
{
_textures = new LinkedList<Texture>();
_deferredRemovals = new ConcurrentQueue<Texture>();
_shortCacheBuilder = new HashSet<ShortTextureCacheEntry>();
_shortCache = new HashSet<ShortTextureCacheEntry>();
_shortCacheLookup = new Dictionary<TextureDescriptor, ShortTextureCacheEntry>();
}
/// <summary>
@ -130,6 +162,85 @@ namespace Ryujinx.Graphics.Gpu.Image
_deferredRemovals.Enqueue(texture);
}
/// <summary>
/// Attempt to find a texture on the short duration cache.
/// </summary>
/// <param name="descriptor">The texture descriptor</param>
/// <returns>The texture if found, null otherwise</returns>
public Texture FindShortCache(in TextureDescriptor descriptor)
{
if (_shortCacheLookup.Count > 0 && _shortCacheLookup.TryGetValue(descriptor, out var entry))
{
if (entry.InvalidatedSequence == entry.Texture.InvalidatedSequence)
{
return entry.Texture;
}
else
{
_shortCacheLookup.Remove(descriptor);
}
}
return null;
}
/// <summary>
/// Removes a texture from the short duration cache.
/// </summary>
/// <param name="texture">Texture to remove from the short cache</param>
public void RemoveShortCache(Texture texture)
{
bool removed = _shortCache.Remove(texture.ShortCacheEntry);
removed |= _shortCacheBuilder.Remove(texture.ShortCacheEntry);
if (removed)
{
texture.DecrementReferenceCount();
_shortCacheLookup.Remove(texture.ShortCacheEntry.Descriptor);
texture.ShortCacheEntry = null;
}
}
/// <summary>
/// Adds a texture to the short duration cache.
/// It starts in the builder set, and it is moved into the deletion set on next process.
/// </summary>
/// <param name="texture">Texture to add to the short cache</param>
/// <param name="descriptor">Last used texture descriptor</param>
public void AddShortCache(Texture texture, ref TextureDescriptor descriptor)
{
var entry = new ShortTextureCacheEntry(descriptor, texture);
_shortCacheBuilder.Add(entry);
_shortCacheLookup.Add(entry.Descriptor, entry);
texture.ShortCacheEntry = entry;
texture.IncrementReferenceCount();
}
/// <summary>
/// Delete textures from the short duration cache.
/// Moves the builder set to be deleted on next process.
/// </summary>
public void ProcessShortCache()
{
HashSet<ShortTextureCacheEntry> toRemove = _shortCache;
foreach (var entry in toRemove)
{
entry.Texture.DecrementReferenceCount();
_shortCacheLookup.Remove(entry.Descriptor);
entry.Texture.ShortCacheEntry = null;
}
toRemove.Clear();
_shortCache = _shortCacheBuilder;
_shortCacheBuilder = toRemove;
}
public IEnumerator<Texture> GetEnumerator()
{
return _textures.GetEnumerator();

View File

@ -91,7 +91,17 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <returns>A reference to the descriptor</returns>
public ref readonly T2 GetDescriptorRef(int id)
{
return ref MemoryMarshal.Cast<byte, T2>(PhysicalMemory.GetSpan(Address + (ulong)id * DescriptorSize, DescriptorSize))[0];
return ref GetDescriptorRefAddress(Address + (ulong)id * DescriptorSize);
}
/// <summary>
/// Gets a reference to the descriptor for a given address.
/// </summary>
/// <param name="address">Address of the descriptor</param>
/// <returns>A reference to the descriptor</returns>
public ref readonly T2 GetDescriptorRefAddress(ulong address)
{
return ref MemoryMarshal.Cast<byte, T2>(PhysicalMemory.GetSpan(address, DescriptorSize))[0];
}
/// <summary>

View File

@ -138,6 +138,10 @@ namespace Ryujinx.Graphics.Gpu.Image
public LinkedListNode<Texture> CacheNode { get; set; }
/// <summary>
/// Entry for this texture in the short duration cache, if present.
/// </summary>
public ShortTextureCacheEntry ShortCacheEntry { get; set; }
/// Physical memory ranges where the texture data is located.
/// </summary>
public MultiRange Range { get; private set; }
@ -1555,6 +1559,20 @@ namespace Ryujinx.Graphics.Gpu.Image
_poolOwners.Add(new TexturePoolOwner { Pool = pool, ID = id });
}
_referenceCount++;
if (ShortCacheEntry != null)
{
_physicalMemory.TextureCache.RemoveShortCache(this);
}
}
/// <summary>
/// Indicates that the texture has one reference left, and will delete on reference decrement.
/// </summary>
/// <returns>True if there is one reference remaining, false otherwise</returns>
public bool HasOneReference()
{
return _referenceCount == 1;
}
/// <summary>
@ -1624,6 +1642,14 @@ namespace Ryujinx.Graphics.Gpu.Image
_poolOwners.Clear();
}
if (ShortCacheEntry != null && _context.IsGpuThread())
{
// If this is called from another thread (unmapped), the short cache will
// have to remove this texture on a future tick.
_physicalMemory.TextureCache.RemoveShortCache(this);
}
InvalidatedSequence++;
}

View File

@ -894,6 +894,16 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <summary>
/// Attempt to find a texture on the short duration cache.
/// </summary>
/// <param name="descriptor">The texture descriptor</param>
/// <returns>The texture if found, null otherwise</returns>
public Texture FindShortCache(in TextureDescriptor descriptor)
{
return _cache.FindShortCache(descriptor);
}
/// <summary>
/// Tries to find an existing texture matching the given buffer copy destination. If none is found, returns null.
/// </summary>
@ -1178,6 +1188,33 @@ namespace Ryujinx.Graphics.Gpu.Image
_cache.RemoveDeferred(texture);
}
/// <summary>
/// Adds a texture to the short duration cache. This typically keeps it alive for two ticks.
/// </summary>
/// <param name="texture">Texture to add to the short cache</param>
/// <param name="descriptor">Last used texture descriptor</param>
public void AddShortCache(Texture texture, ref TextureDescriptor descriptor)
{
_cache.AddShortCache(texture, ref descriptor);
}
/// <summary>
/// Removes a texture from the short duration cache.
/// </summary>
/// <param name="texture">Texture to remove from the short cache</param>
public void RemoveShortCache(Texture texture)
{
_cache.RemoveShortCache(texture);
}
/// <summary>
/// Ticks periodic elements of the texture cache.
/// </summary>
public void Tick()
{
_cache.ProcessShortCache();
}
/// <summary>
/// Disposes all textures and samplers in the cache.
/// It's an error to use the texture cache after disposal.

View File

@ -1,3 +1,4 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
@ -6,7 +7,7 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <summary>
/// Maxwell texture descriptor, as stored on the GPU texture pool memory region.
/// </summary>
struct TextureDescriptor : ITextureDescriptor
struct TextureDescriptor : ITextureDescriptor, IEquatable<TextureDescriptor>
{
#pragma warning disable CS0649
public uint Word0;
@ -249,5 +250,24 @@ namespace Ryujinx.Graphics.Gpu.Image
{
return Unsafe.As<TextureDescriptor, Vector256<byte>>(ref this).Equals(Unsafe.As<TextureDescriptor, Vector256<byte>>(ref other));
}
/// <summary>
/// Check if two descriptors are equal.
/// </summary>
/// <param name="other">The descriptor to compare against</param>
/// <returns>True if they are equal, false otherwise</returns>
public bool Equals(TextureDescriptor other)
{
return Equals(ref other);
}
/// <summary>
/// Gets a hash code for this descriptor.
/// </summary>
/// <returns>The hash code for this descriptor.</returns>
public override int GetHashCode()
{
return Unsafe.As<TextureDescriptor, Vector256<byte>>(ref this).GetHashCode();
}
}
}

View File

@ -52,16 +52,21 @@ namespace Ryujinx.Graphics.Gpu.Image
if (texture == null)
{
TextureInfo info = GetInfo(descriptor, out int layerSize);
texture = PhysicalMemory.TextureCache.FindShortCache(descriptor);
ProcessDereferenceQueue();
texture = PhysicalMemory.TextureCache.FindOrCreateTexture(_channel.MemoryManager, TextureSearchFlags.ForSampler, info, layerSize);
// If this happens, then the texture address is invalid, we can't add it to the cache.
if (texture == null)
{
return ref descriptor;
TextureInfo info = GetInfo(descriptor, out int layerSize);
ProcessDereferenceQueue();
texture = PhysicalMemory.TextureCache.FindOrCreateTexture(_channel.MemoryManager, TextureSearchFlags.ForSampler, info, layerSize);
// If this happens, then the texture address is invalid, we can't add it to the cache.
if (texture == null)
{
return ref descriptor;
}
}
texture.IncrementReferenceCount(this, id);
@ -208,15 +213,21 @@ namespace Ryujinx.Graphics.Gpu.Image
if (texture != null)
{
TextureDescriptor descriptor = PhysicalMemory.Read<TextureDescriptor>(address);
ref TextureDescriptor cachedDescriptor = ref DescriptorCache[id];
ref readonly TextureDescriptor descriptor = ref GetDescriptorRefAddress(address);
// If the descriptors are the same, the texture is the same,
// we don't need to remove as it was not modified. Just continue.
if (descriptor.Equals(ref DescriptorCache[id]))
if (descriptor.Equals(ref cachedDescriptor))
{
continue;
}
if (texture.HasOneReference())
{
_channel.MemoryManager.Physical.TextureCache.AddShortCache(texture, ref cachedDescriptor);
}
texture.DecrementReferenceCount(this, id);
Items[id] = null;

View File

@ -204,6 +204,8 @@ namespace Ryujinx.Graphics.Gpu
Texture texture = pt.Cache.FindOrCreateTexture(null, TextureSearchFlags.WithUpscale, pt.Info, 0, null, pt.Range);
pt.Cache.Tick();
texture.SynchronizeMemory();
ImageCrop crop = pt.Crop;

View File

@ -79,21 +79,7 @@ namespace Ryujinx.Graphics.Vulkan
var sampleCountFlags = ConvertToSampleCountFlags(gd.Capabilities.SupportedSampleCounts, (uint)info.Samples);
var usage = DefaultUsageFlags;
if (info.Format.IsDepthOrStencil())
{
usage |= ImageUsageFlags.DepthStencilAttachmentBit;
}
else if (info.Format.IsRtColorCompatible())
{
usage |= ImageUsageFlags.ColorAttachmentBit;
}
if (info.Format.IsImageCompatible())
{
usage |= ImageUsageFlags.StorageBit;
}
var usage = GetImageUsageFromFormat(info.Format);
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)
{
if (samples == 0 || samples > (uint)SampleCountFlags.Count64Bit)

View File

@ -54,6 +54,7 @@ namespace Ryujinx.Graphics.Vulkan
gd.Textures.Add(this);
var format = _gd.FormatCapabilities.ConvertToVkFormat(info.Format);
var usage = TextureStorage.GetImageUsageFromFormat(info.Format);
var levels = (uint)info.Levels;
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 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()
{
@ -110,14 +111,14 @@ namespace Ryujinx.Graphics.Vulkan
Format = format,
Components = cm,
SubresourceRange = sr,
PNext = usageFlags == 0 ? null : &usage
PNext = &usage
};
gd.Api.CreateImageView(device, imageCreateInfo, null, out var imageView).ThrowOnError();
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.
var identityComponentMapping = new ComponentMapping(
@ -126,7 +127,7 @@ namespace Ryujinx.Graphics.Vulkan
ComponentSwizzle.B,
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.
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);
_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.Threading;
using Ryujinx.Horizon.Common;
@ -71,4 +70,4 @@ namespace Ryujinx.HLE.HOS.Kernel
return null;
}
}
}
}

View File

@ -10,6 +10,24 @@ namespace Ryujinx.HLE.HOS.Services.Pm
{
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)]
// 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)

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")]
class IInformationInterface : IpcService
{
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[] _nsoIcon;
private VirtualFileSystem _virtualFileSystem;
private Language _desiredTitleLanguage;
private CancellationTokenSource _cancellationToken;
private readonly VirtualFileSystem _virtualFileSystem;
private Language _desiredTitleLanguage;
private CancellationTokenSource _cancellationToken;
public ApplicationLibrary(VirtualFileSystem virtualFileSystem)
{
@ -53,7 +53,7 @@ namespace Ryujinx.Ui.App.Common
_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);
byte[] resourceByteArray = new byte[resourceStream.Length];
@ -68,9 +68,9 @@ namespace Ryujinx.Ui.App.Common
_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();
controlFile.Get.Read(out _, 0, outProperty, ReadOption.None).ThrowIfFailure();
@ -86,7 +86,7 @@ namespace Ryujinx.Ui.App.Common
_cancellationToken = new CancellationTokenSource();
// Builds the applications list with paths to found applications
List<string> applications = new List<string>();
List<string> applications = new();
try
{
@ -143,251 +143,246 @@ namespace Ryujinx.Ui.App.Common
string version = "0";
byte[] applicationIcon = null;
BlitStruct<ApplicationControlProperty> controlHolder = new BlitStruct<ApplicationControlProperty>(1);
BlitStruct<ApplicationControlProperty> controlHolder = new(1);
try
{
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());
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")
{
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();
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;
break;
}
}
if (!hasMainNca && !isExeFs)
else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main")
{
numApplicationsFound--;
continue;
isExeFs = true;
}
}
if (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()))
if (!hasMainNca && !isExeFs)
{
numApplicationsFound--;
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--;
continue;
}
applicationIcon = _ncaIcon;
titleName = Path.GetFileNameWithoutExtension(applicationPath);
}
// If its an NSO we just set defaults
else if (extension == ".nso")
catch (InvalidDataException)
{
applicationIcon = _nsoIcon;
titleName = Path.GetFileNameWithoutExtension(applicationPath);
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}");
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)
@ -404,14 +399,21 @@ namespace Ryujinx.Ui.App.Common
appMetadata.Title = titleName;
});
if (appMetadata.LastPlayed != "Never" && !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?)");
if (appMetadata.LastPlayed != "Never")
{
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,
Icon = applicationIcon,
@ -419,7 +421,7 @@ namespace Ryujinx.Ui.App.Common
TitleId = titleId,
Developer = developer,
Version = version,
TimePlayed = ConvertSecondsToReadableString(appMetadata.TimePlayed),
TimePlayed = ConvertSecondsToFormattedString(appMetadata.TimePlayed),
TimePlayedNum = appMetadata.TimePlayed,
LastPlayed = appMetadata.LastPlayed,
FileExtension = Path.GetExtension(applicationPath).ToUpper().Remove(0, 1),
@ -488,10 +490,9 @@ namespace Ryujinx.Ui.App.Common
appMetadata = new ApplicationMetadata();
using (FileStream stream = File.Create(metadataFile, 4096, FileOptions.WriteThrough))
{
JsonHelper.Serialize(stream, appMetadata, true);
}
using FileStream stream = File.Create(metadataFile, 4096, FileOptions.WriteThrough);
JsonHelper.Serialize(stream, appMetadata, true);
}
try
@ -509,10 +510,9 @@ namespace Ryujinx.Ui.App.Common
{
modifyFunction(appMetadata);
using (FileStream stream = File.Create(metadataFile, 4096, FileOptions.WriteThrough))
{
JsonHelper.Serialize(stream, appMetadata, true);
}
using FileStream stream = File.Create(metadataFile, 4096, FileOptions.WriteThrough);
JsonHelper.Serialize(stream, appMetadata, true);
}
return appMetadata;
@ -529,192 +529,177 @@ namespace Ryujinx.Ui.App.Common
{
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());
pfs = xci.OpenPartition(XciPartitionType.Secure);
}
else
{
pfs = new PartitionFileSystem(file.AsStorage());
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*"))
if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main")
{
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
GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out _);
// Read the icon from the ControlFS and store it as a byte array
try
foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*"))
{
if (entry.Name == "control.nacp")
{
continue;
}
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);
applicationIcon = stream.ToArray();
}
}
catch (HorizonResultException)
{
foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*"))
if (applicationIcon != null)
{
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;
break;
}
}
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);
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}");
}
applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon;
}
else if (extension == ".nca")
catch (InvalidDataException)
{
applicationIcon = _ncaIcon;
applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon;
}
// If its an NSO we just set defaults
else if (extension == ".nso")
catch (Exception exception)
{
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)
{
Logger.Warning?.Print(LogClass.Application,
$"Could not retrieve a valid icon for the app. Default icon will be used. Errored File: {applicationPath}");
Logger.Warning?.Print(LogClass.Application, $"Could not retrieve a valid icon for the app. Default icon will be used. Errored File: {applicationPath}");
}
return applicationIcon ?? _ncaIcon;
}
private string ConvertSecondsToReadableString(double seconds)
private static string ConvertSecondsToFormattedString(double seconds)
{
const int secondsPerMinute = 60;
const int secondsPerHour = secondsPerMinute * 60;
const int secondsPerDay = secondsPerHour * 24;
System.TimeSpan time = System.TimeSpan.FromSeconds(seconds);
string readableString;
if (seconds < secondsPerMinute)
string timeString;
if (time.Days != 0)
{
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
{
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)
@ -797,8 +782,7 @@ namespace Ryujinx.Ui.App.Common
}
catch (InvalidDataException)
{
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}");
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}");
}
catch (MissingKeyException exception)
{

View File

@ -1,19 +1,75 @@
using Ryujinx.Common.Logging;
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
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)
{
Process.Start(new ProcessStartInfo
if (Directory.Exists(path))
{
FileName = path,
UseShellExecute = true,
Verb = "open"
});
Process.Start(new ProcessStartInfo
{
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)

View File

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