Compare commits

...

12 Commits

Author SHA1 Message Date
Ac_K
64263c5218 UI: Fix applications times (#4294)
* Fix applications times

* Add spaces

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

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

Fixes #2516

* Null check + Proper result code

* Better comment

* Implement `GetApplicationProcessId`

* Add TODOs

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

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

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

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

* Remove new function from KernelStatic

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

* Dialogue opens

* Changes

* Switch to ListBox

* Fix bugs and stuff

* Fix spacing

* Implement OpenLocation

* Change icon

* Color

* Color

* Remove background

* Make no update the same height

* Fix height and smooth scroll

* Height

* Fix update selection

* Make window smaller

* Add back remove all button

* Make selection more obvious

* Hide selection bar on SaveManager

* Fix autoscroll

* Fix no update not staying selected

* Better file opener

* Fix

* Revert that

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

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

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

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

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

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

* Log warning

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

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

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

* prefer shifting to modulo

* prefer MathF

* fix nits

* rm ResampleForUpsampler

* oop

* Array20

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

* Add check in MouseDriver and reduce the time of idling

* Fix linux error

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -44,8 +44,10 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Runtime.Versioning;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop;
using Image = SixLabors.ImageSharp.Image; using Image = SixLabors.ImageSharp.Image;
using InputManager = Ryujinx.Input.HLE.InputManager; using InputManager = Ryujinx.Input.HLE.InputManager;
using Key = Ryujinx.Input.Key; using Key = Ryujinx.Input.Key;
@@ -58,12 +60,14 @@ namespace Ryujinx.Ava
{ {
internal class AppHost internal class AppHost
{ {
private const int CursorHideIdleTime = 8; // Hide Cursor seconds. private const int CursorHideIdleTime = 5; // Hide Cursor seconds.
private const float MaxResolutionScale = 4.0f; // Max resolution hotkeys can scale to before wrapping. private const float MaxResolutionScale = 4.0f; // Max resolution hotkeys can scale to before wrapping.
private const int TargetFps = 60; private const int TargetFps = 60;
private const float VolumeDelta = 0.05f; private const float VolumeDelta = 0.05f;
private static readonly Cursor InvisibleCursor = new(StandardCursorType.None); private static readonly Cursor InvisibleCursor = new(StandardCursorType.None);
private readonly IntPtr InvisibleCursorWin;
private readonly IntPtr DefaultCursorWin;
private readonly long _ticksPerFrame; private readonly long _ticksPerFrame;
private readonly Stopwatch _chrono; private readonly Stopwatch _chrono;
@@ -81,7 +85,6 @@ namespace Ryujinx.Ava
private float _newVolume; private float _newVolume;
private KeyboardHotkeyState _prevHotkeyState; private KeyboardHotkeyState _prevHotkeyState;
private bool _hideCursorOnIdle;
private long _lastCursorMoveTime; private long _lastCursorMoveTime;
private bool _isCursorInRenderer; private bool _isCursorInRenderer;
@@ -131,7 +134,6 @@ namespace Ryujinx.Ava
_accountManager = accountManager; _accountManager = accountManager;
_userChannelPersistence = userChannelPersistence; _userChannelPersistence = userChannelPersistence;
_renderingThread = new Thread(RenderLoop, 1 * 1024 * 1024) { Name = "GUI.RenderThread" }; _renderingThread = new Thread(RenderLoop, 1 * 1024 * 1024) { Name = "GUI.RenderThread" };
_hideCursorOnIdle = ConfigurationState.Instance.HideCursorOnIdle;
_lastCursorMoveTime = Stopwatch.GetTimestamp(); _lastCursorMoveTime = Stopwatch.GetTimestamp();
_glLogLevel = ConfigurationState.Instance.Logger.GraphicsDebugLevel; _glLogLevel = ConfigurationState.Instance.Logger.GraphicsDebugLevel;
_topLevel = topLevel; _topLevel = topLevel;
@@ -159,9 +161,14 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.HideCursorOnIdle.Event += HideCursorState_Changed; ConfigurationState.Instance.HideCursorOnIdle.Event += HideCursorState_Changed;
_topLevel.PointerLeave += TopLevel_PointerLeave;
_topLevel.PointerMoved += TopLevel_PointerMoved; _topLevel.PointerMoved += TopLevel_PointerMoved;
if (OperatingSystem.IsWindows())
{
InvisibleCursorWin = CreateEmptyCursor();
DefaultCursorWin = CreateArrowCursor();
}
ConfigurationState.Instance.System.IgnoreMissingServices.Event += UpdateIgnoreMissingServicesState; ConfigurationState.Instance.System.IgnoreMissingServices.Event += UpdateIgnoreMissingServicesState;
ConfigurationState.Instance.Graphics.AspectRatio.Event += UpdateAspectRatioState; ConfigurationState.Instance.Graphics.AspectRatio.Event += UpdateAspectRatioState;
ConfigurationState.Instance.System.EnableDockedMode.Event += UpdateDockedModeState; ConfigurationState.Instance.System.EnableDockedMode.Event += UpdateDockedModeState;
@@ -172,20 +179,47 @@ namespace Ryujinx.Ava
private void TopLevel_PointerMoved(object sender, PointerEventArgs e) private void TopLevel_PointerMoved(object sender, PointerEventArgs e)
{ {
if (sender is Control visual) if (sender is MainWindow window)
{ {
_lastCursorMoveTime = Stopwatch.GetTimestamp(); _lastCursorMoveTime = Stopwatch.GetTimestamp();
var point = e.GetCurrentPoint(visual).Position; if ((Renderer.Content as EmbeddedWindow).TransformedBounds != null)
{
var point = e.GetCurrentPoint(window).Position;
var bounds = (Renderer.Content as EmbeddedWindow).TransformedBounds.Value.Clip;
_isCursorInRenderer = Equals(visual.InputHitTest(point), Renderer); _isCursorInRenderer = point.X >= bounds.X &&
point.X <= bounds.Width + bounds.X &&
point.Y >= bounds.Y &&
point.Y <= bounds.Height + bounds.Y;
}
} }
} }
private void TopLevel_PointerLeave(object sender, PointerEventArgs e) private void ShowCursor()
{ {
_isCursorInRenderer = false; Dispatcher.UIThread.Post(() =>
_viewModel.Cursor = Cursor.Default; {
_viewModel.Cursor = Cursor.Default;
if (OperatingSystem.IsWindows())
{
SetCursor(DefaultCursorWin);
}
});
}
private void HideCursor()
{
Dispatcher.UIThread.Post(() =>
{
_viewModel.Cursor = InvisibleCursor;
if (OperatingSystem.IsWindows())
{
SetCursor(InvisibleCursorWin);
}
});
} }
private void SetRendererWindowSize(Size size) private void SetRendererWindowSize(Size size)
@@ -380,7 +414,6 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.System.EnableDockedMode.Event -= UpdateDockedModeState; ConfigurationState.Instance.System.EnableDockedMode.Event -= UpdateDockedModeState;
ConfigurationState.Instance.System.AudioVolume.Event -= UpdateAudioVolumeState; ConfigurationState.Instance.System.AudioVolume.Event -= UpdateAudioVolumeState;
_topLevel.PointerLeave -= TopLevel_PointerLeave;
_topLevel.PointerMoved -= TopLevel_PointerMoved; _topLevel.PointerMoved -= TopLevel_PointerMoved;
_gpuCancellationTokenSource.Cancel(); _gpuCancellationTokenSource.Cancel();
@@ -406,19 +439,10 @@ namespace Ryujinx.Ava
private void HideCursorState_Changed(object sender, ReactiveEventArgs<bool> state) private void HideCursorState_Changed(object sender, ReactiveEventArgs<bool> state)
{ {
Dispatcher.UIThread.InvokeAsync(delegate if (state.NewValue)
{ {
_hideCursorOnIdle = state.NewValue; _lastCursorMoveTime = Stopwatch.GetTimestamp();
}
if (_hideCursorOnIdle)
{
_lastCursorMoveTime = Stopwatch.GetTimestamp();
}
else
{
_viewModel.Cursor = Cursor.Default;
}
});
} }
public async Task<bool> LoadGuestApplication() public async Task<bool> LoadGuestApplication()
@@ -860,29 +884,6 @@ namespace Ryujinx.Ava
} }
} }
private void HandleScreenState()
{
if (ConfigurationState.Instance.Hid.EnableMouse)
{
Dispatcher.UIThread.Post(() =>
{
_viewModel.Cursor = _isCursorInRenderer ? InvisibleCursor : Cursor.Default;
});
}
else
{
if (_hideCursorOnIdle)
{
long cursorMoveDelta = Stopwatch.GetTimestamp() - _lastCursorMoveTime;
Dispatcher.UIThread.Post(() =>
{
_viewModel.Cursor = cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency ? InvisibleCursor : Cursor.Default;
});
}
}
}
private bool UpdateFrame() private bool UpdateFrame()
{ {
if (!_isActive) if (!_isActive)
@@ -890,23 +891,44 @@ namespace Ryujinx.Ava
return false; return false;
} }
NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
if (_viewModel.IsActive) if (_viewModel.IsActive)
{ {
if (ConfigurationState.Instance.Hid.EnableMouse)
{
if (_isCursorInRenderer)
{
HideCursor();
}
else
{
ShowCursor();
}
}
else
{
if (ConfigurationState.Instance.HideCursorOnIdle)
{
if (Stopwatch.GetTimestamp() - _lastCursorMoveTime >= CursorHideIdleTime * Stopwatch.Frequency)
{
HideCursor();
}
else
{
ShowCursor();
}
}
}
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
{ {
HandleScreenState();
if (_keyboardInterface.GetKeyboardStateSnapshot().IsPressed(Key.Delete) && _viewModel.WindowState != WindowState.FullScreen) if (_keyboardInterface.GetKeyboardStateSnapshot().IsPressed(Key.Delete) && _viewModel.WindowState != WindowState.FullScreen)
{ {
Device.Application.DiskCacheLoadState?.Cancel(); Device.Application.DiskCacheLoadState?.Cancel();
} }
}); });
}
NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
if (_viewModel.IsActive)
{
KeyboardHotkeyState currentHotkeyState = GetHotkeyState(); KeyboardHotkeyState currentHotkeyState = GetHotkeyState();
if (currentHotkeyState != _prevHotkeyState) if (currentHotkeyState != _prevHotkeyState)

View File

@@ -524,7 +524,7 @@
"UserErrorUndefinedDescription": "An undefined error occured! This shouldn't happen, please contact a dev!", "UserErrorUndefinedDescription": "An undefined error occured! This shouldn't happen, please contact a dev!",
"OpenSetupGuideMessage": "Open the Setup Guide", "OpenSetupGuideMessage": "Open the Setup Guide",
"NoUpdate": "No Update", "NoUpdate": "No Update",
"TitleUpdateVersionLabel": "Version {0} - {1}", "TitleUpdateVersionLabel": "Version {0}",
"RyujinxInfo": "Ryujinx - Info", "RyujinxInfo": "Ryujinx - Info",
"RyujinxConfirm": "Ryujinx - Confirmation", "RyujinxConfirm": "Ryujinx - Confirmation",
"FileDialogAllTypes": "All types", "FileDialogAllTypes": "All types",
@@ -585,7 +585,7 @@
"UserProfilesSetProfileImage": "Set Profile Image", "UserProfilesSetProfileImage": "Set Profile Image",
"UserProfileEmptyNameError": "Name is required", "UserProfileEmptyNameError": "Name is required",
"UserProfileNoImageError": "Profile image must be set", "UserProfileNoImageError": "Profile image must be set",
"GameUpdateWindowHeading": "{0} Update(s) available for {1} ({2})", "GameUpdateWindowHeading": "Manage Updates for {0} ({1})",
"SettingsTabHotkeysResScaleUpHotkey": "Increase resolution:", "SettingsTabHotkeysResScaleUpHotkey": "Increase resolution:",
"SettingsTabHotkeysResScaleDownHotkey": "Decrease resolution:", "SettingsTabHotkeysResScaleDownHotkey": "Decrease resolution:",
"UserProfilesName": "Name:", "UserProfilesName": "Name:",

View File

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

View File

@@ -28,7 +28,7 @@ namespace Ryujinx.Ava
public static double DesktopScaleFactor { get; set; } = 1.0; public static double DesktopScaleFactor { get; set; } = 1.0;
public static string Version { get; private set; } public static string Version { get; private set; }
public static string ConfigurationPath { get; private set; } public static string ConfigurationPath { get; private set; }
public static bool PreviewerDetached { get; private set; } public static bool PreviewerDetached { get; private set; }
[LibraryImport("user32.dll", SetLastError = true)] [LibraryImport("user32.dll", SetLastError = true)]
public static partial int MessageBoxA(IntPtr hWnd, [MarshalAs(UnmanagedType.LPStr)] string text, [MarshalAs(UnmanagedType.LPStr)] string caption, uint type); public static partial int MessageBoxA(IntPtr hWnd, [MarshalAs(UnmanagedType.LPStr)] string text, [MarshalAs(UnmanagedType.LPStr)] string caption, uint type);

View File

@@ -34,6 +34,8 @@ namespace Ryujinx.Ava.UI.Helpers
{ {
WindowHandle = IntPtr.Zero; WindowHandle = IntPtr.Zero;
X11Display = IntPtr.Zero; X11Display = IntPtr.Zero;
NsView = IntPtr.Zero;
MetalLayer = IntPtr.Zero;
} }
public EmbeddedWindow() public EmbeddedWindow()
@@ -42,7 +44,7 @@ namespace Ryujinx.Ava.UI.Helpers
stateObserverable.Subscribe(StateChanged); stateObserverable.Subscribe(StateChanged);
this.Initialized += NativeEmbeddedWindow_Initialized; Initialized += NativeEmbeddedWindow_Initialized;
} }
public virtual void OnWindowCreated() { } public virtual void OnWindowCreated() { }
@@ -127,7 +129,7 @@ namespace Ryujinx.Ava.UI.Helpers
lpfnWndProc = Marshal.GetFunctionPointerForDelegate(_wndProcDelegate), lpfnWndProc = Marshal.GetFunctionPointerForDelegate(_wndProcDelegate),
style = ClassStyles.CS_OWNDC, style = ClassStyles.CS_OWNDC,
lpszClassName = Marshal.StringToHGlobalUni(_className), lpszClassName = Marshal.StringToHGlobalUni(_className),
hCursor = LoadCursor(IntPtr.Zero, (IntPtr)Cursors.IDC_ARROW) hCursor = CreateArrowCursor()
}; };
var atom = RegisterClassEx(ref wndClassEx); var atom = RegisterClassEx(ref wndClassEx);
@@ -198,6 +200,7 @@ namespace Ryujinx.Ava.UI.Helpers
KeyModifiers.None)); KeyModifiers.None));
break; break;
} }
return DefWindowProc(hWnd, msg, wParam, lParam); return DefWindowProc(hWnd, msg, wParam, lParam);
} }

View File

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

View File

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

View File

@@ -1601,13 +1601,9 @@ namespace Ryujinx.Ava.UI.ViewModels
public async void OpenTitleUpdateManager() public async void OpenTitleUpdateManager()
{ {
ApplicationData selection = SelectedApplication; if (SelectedApplication != null)
if (selection != null)
{ {
if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) await TitleUpdateWindow.Show(VirtualFileSystem, ulong.Parse(SelectedApplication.TitleId, NumberStyles.HexNumber), SelectedApplication.TitleName);
{
await new TitleUpdateWindow(VirtualFileSystem, ulong.Parse(selection.TitleId, NumberStyles.HexNumber), selection.TitleName).ShowDialog(desktop.MainWindow);
}
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -73,7 +73,7 @@ namespace Ryujinx.Graphics.Gpu
// Since the memory manager changed, make sure we will get pools from addresses of the new memory manager. // Since the memory manager changed, make sure we will get pools from addresses of the new memory manager.
TextureManager.ReloadPools(); TextureManager.ReloadPools();
MemoryManager.Physical.BufferCache.QueuePrune(); memoryManager.Physical.BufferCache.QueuePrune();
} }
/// <summary> /// <summary>
@@ -84,7 +84,9 @@ namespace Ryujinx.Graphics.Gpu
private void MemoryUnmappedHandler(object sender, UnmapEventArgs e) private void MemoryUnmappedHandler(object sender, UnmapEventArgs e)
{ {
TextureManager.ReloadPools(); TextureManager.ReloadPools();
MemoryManager.Physical.BufferCache.QueuePrune();
var memoryManager = Volatile.Read(ref _memoryManager);
memoryManager?.Physical.BufferCache.QueuePrune();
} }
/// <summary> /// <summary>

View File

@@ -1431,9 +1431,20 @@ namespace Ryujinx.Graphics.Gpu.Image
return; return;
} }
bool isGpuThread = _context.IsGpuThread();
if (isGpuThread)
{
// No need to wait if we're on the GPU thread, we can just clear the modified flag immediately.
handle.Modified = false;
}
_context.Renderer.BackgroundContextAction(() => _context.Renderer.BackgroundContextAction(() =>
{ {
handle.Sync(_context); if (!isGpuThread)
{
handle.Sync(_context);
}
Storage.SignalModifiedDirty(); Storage.SignalModifiedDirty();

View File

@@ -232,32 +232,23 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <param name="context">The GPU context used to wait for sync</param> /// <param name="context">The GPU context used to wait for sync</param>
public void Sync(GpuContext context) public void Sync(GpuContext context)
{ {
bool needsSync = !context.IsGpuThread(); ulong registeredSync = _registeredSync;
long diff = (long)(context.SyncNumber - registeredSync);
if (needsSync) if (diff > 0)
{ {
ulong registeredSync = _registeredSync; context.Renderer.WaitSync(registeredSync);
long diff = (long)(context.SyncNumber - registeredSync);
if (diff > 0) if ((long)(_modifiedSync - registeredSync) > 0)
{ {
context.Renderer.WaitSync(registeredSync); // Flush the data in a previous state. Do not remove the modified flag - it will be removed to ignore following writes.
return;
if ((long)(_modifiedSync - registeredSync) > 0)
{
// Flush the data in a previous state. Do not remove the modified flag - it will be removed to ignore following writes.
return;
}
Modified = false;
} }
// If the difference is <= 0, no data is not ready yet. Flush any data we can without waiting or removing modified flag.
}
else
{
Modified = false; Modified = false;
} }
// If the difference is <= 0, no data is not ready yet. Flush any data we can without waiting or removing modified flag.
} }
/// <summary> /// <summary>

View File

@@ -14,12 +14,6 @@ namespace Ryujinx.Graphics.Vulkan
MemoryPropertyFlags.HostCoherentBit | MemoryPropertyFlags.HostCoherentBit |
MemoryPropertyFlags.HostCachedBit; MemoryPropertyFlags.HostCachedBit;
// Some drivers don't expose a "HostCached" memory type,
// so we need those alternative flags for the allocation to succeed there.
private const MemoryPropertyFlags DefaultBufferMemoryAltFlags =
MemoryPropertyFlags.HostVisibleBit |
MemoryPropertyFlags.HostCoherentBit;
private const MemoryPropertyFlags DeviceLocalBufferMemoryFlags = private const MemoryPropertyFlags DeviceLocalBufferMemoryFlags =
MemoryPropertyFlags.DeviceLocalBit; MemoryPropertyFlags.DeviceLocalBit;
@@ -100,21 +94,9 @@ namespace Ryujinx.Graphics.Vulkan
gd.Api.CreateBuffer(_device, in bufferCreateInfo, null, out var buffer).ThrowOnError(); gd.Api.CreateBuffer(_device, in bufferCreateInfo, null, out var buffer).ThrowOnError();
gd.Api.GetBufferMemoryRequirements(_device, buffer, out var requirements); gd.Api.GetBufferMemoryRequirements(_device, buffer, out var requirements);
MemoryPropertyFlags allocateFlags; var allocateFlags = deviceLocal ? DeviceLocalBufferMemoryFlags : DefaultBufferMemoryFlags;
MemoryPropertyFlags allocateFlagsAlt;
if (deviceLocal) var allocation = gd.MemoryAllocator.AllocateDeviceMemory(_physicalDevice, requirements, allocateFlags);
{
allocateFlags = DeviceLocalBufferMemoryFlags;
allocateFlagsAlt = DeviceLocalBufferMemoryFlags;
}
else
{
allocateFlags = DefaultBufferMemoryFlags;
allocateFlagsAlt = DefaultBufferMemoryAltFlags;
}
var allocation = gd.MemoryAllocator.AllocateDeviceMemory(_physicalDevice, requirements, allocateFlags, allocateFlagsAlt);
if (allocation.Memory.Handle == 0UL) if (allocation.Memory.Handle == 0UL)
{ {

View File

@@ -23,7 +23,7 @@ namespace Ryujinx.Graphics.Vulkan
public int[] AttachmentIndices { get; } public int[] AttachmentIndices { get; }
public int AttachmentsCount { get; } public int AttachmentsCount { get; }
public int MaxColorAttachmentIndex => AttachmentIndices.Length > 0 ? AttachmentIndices[AttachmentIndices.Length - 1] : -1; public int MaxColorAttachmentIndex { get; }
public bool HasDepthStencil { get; } public bool HasDepthStencil { get; }
public int ColorAttachmentsCount => AttachmentsCount - (HasDepthStencil ? 1 : 0); public int ColorAttachmentsCount => AttachmentsCount - (HasDepthStencil ? 1 : 0);
@@ -67,6 +67,7 @@ namespace Ryujinx.Graphics.Vulkan
AttachmentSamples = new uint[count]; AttachmentSamples = new uint[count];
AttachmentFormats = new VkFormat[count]; AttachmentFormats = new VkFormat[count];
AttachmentIndices = new int[count]; AttachmentIndices = new int[count];
MaxColorAttachmentIndex = colors.Length - 1;
uint width = uint.MaxValue; uint width = uint.MaxValue;
uint height = uint.MaxValue; uint height = uint.MaxValue;

View File

@@ -27,16 +27,7 @@ namespace Ryujinx.Graphics.Vulkan
MemoryRequirements requirements, MemoryRequirements requirements,
MemoryPropertyFlags flags = 0) MemoryPropertyFlags flags = 0)
{ {
return AllocateDeviceMemory(physicalDevice, requirements, flags, flags); int memoryTypeIndex = FindSuitableMemoryTypeIndex(_api, physicalDevice, requirements.MemoryTypeBits, flags);
}
public MemoryAllocation AllocateDeviceMemory(
PhysicalDevice physicalDevice,
MemoryRequirements requirements,
MemoryPropertyFlags flags,
MemoryPropertyFlags alternativeFlags)
{
int memoryTypeIndex = FindSuitableMemoryTypeIndex(_api, physicalDevice, requirements.MemoryTypeBits, flags, alternativeFlags);
if (memoryTypeIndex < 0) if (memoryTypeIndex < 0)
{ {
return default; return default;
@@ -65,35 +56,21 @@ namespace Ryujinx.Graphics.Vulkan
return newBl.Allocate(size, alignment, map); return newBl.Allocate(size, alignment, map);
} }
private static int FindSuitableMemoryTypeIndex( private static int FindSuitableMemoryTypeIndex(Vk api, PhysicalDevice physicalDevice, uint memoryTypeBits, MemoryPropertyFlags flags)
Vk api,
PhysicalDevice physicalDevice,
uint memoryTypeBits,
MemoryPropertyFlags flags,
MemoryPropertyFlags alternativeFlags)
{ {
int bestCandidateIndex = -1;
api.GetPhysicalDeviceMemoryProperties(physicalDevice, out var properties); api.GetPhysicalDeviceMemoryProperties(physicalDevice, out var properties);
for (int i = 0; i < properties.MemoryTypeCount; i++) for (int i = 0; i < properties.MemoryTypeCount; i++)
{ {
var type = properties.MemoryTypes[i]; var type = properties.MemoryTypes[i];
if ((memoryTypeBits & (1 << i)) != 0) if ((memoryTypeBits & (1 << i)) != 0 && type.PropertyFlags.HasFlag(flags))
{ {
if (type.PropertyFlags.HasFlag(flags)) return i;
{
return i;
}
else if (type.PropertyFlags.HasFlag(alternativeFlags))
{
bestCandidateIndex = i;
}
} }
} }
return bestCandidateIndex; return -1;
} }
public void Dispose() public void Dispose()

View File

@@ -1344,7 +1344,8 @@ namespace Ryujinx.Graphics.Vulkan
var dstAttachmentFormats = _newState.Internal.AttachmentFormats.AsSpan(); var dstAttachmentFormats = _newState.Internal.AttachmentFormats.AsSpan();
FramebufferParams.AttachmentFormats.CopyTo(dstAttachmentFormats); FramebufferParams.AttachmentFormats.CopyTo(dstAttachmentFormats);
for (int i = FramebufferParams.AttachmentFormats.Length; i < dstAttachmentFormats.Length; i++) int maxAttachmentIndex = FramebufferParams.MaxColorAttachmentIndex + (FramebufferParams.HasDepthStencil ? 1 : 0);
for (int i = FramebufferParams.AttachmentFormats.Length; i <= maxAttachmentIndex; i++)
{ {
dstAttachmentFormats[i] = 0; dstAttachmentFormats[i] = 0;
} }

View File

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

View File

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

View File

@@ -9,7 +9,6 @@ namespace Ryujinx.Graphics.Vulkan
Intel, Intel,
Nvidia, Nvidia,
ARM, ARM,
Broadcom,
Qualcomm, Qualcomm,
Apple, Apple,
Unknown Unknown
@@ -29,7 +28,6 @@ namespace Ryujinx.Graphics.Vulkan
0x106B => Vendor.Apple, 0x106B => Vendor.Apple,
0x10DE => Vendor.Nvidia, 0x10DE => Vendor.Nvidia,
0x13B5 => Vendor.ARM, 0x13B5 => Vendor.ARM,
0x14E4 => Vendor.Broadcom,
0x8086 => Vendor.Intel, 0x8086 => Vendor.Intel,
0x5143 => Vendor.Qualcomm, 0x5143 => Vendor.Qualcomm,
_ => Vendor.Unknown _ => Vendor.Unknown
@@ -45,7 +43,6 @@ namespace Ryujinx.Graphics.Vulkan
0x106B => "Apple", 0x106B => "Apple",
0x10DE => "NVIDIA", 0x10DE => "NVIDIA",
0x13B5 => "ARM", 0x13B5 => "ARM",
0x14E4 => "Broadcom",
0x1AE0 => "Google", 0x1AE0 => "Google",
0x5143 => "Qualcomm", 0x5143 => "Qualcomm",
0x8086 => "Intel", 0x8086 => "Intel",

View File

@@ -162,6 +162,7 @@ namespace Ryujinx.Graphics.Vulkan
if (messageSeverity.HasFlag(DebugUtilsMessageSeverityFlagsEXT.ErrorBitExt)) if (messageSeverity.HasFlag(DebugUtilsMessageSeverityFlagsEXT.ErrorBitExt))
{ {
Logger.Error?.Print(LogClass.Gpu, msg); Logger.Error?.Print(LogClass.Gpu, msg);
//throw new Exception(msg);
} }
else if (messageSeverity.HasFlag(DebugUtilsMessageSeverityFlagsEXT.WarningBitExt)) else if (messageSeverity.HasFlag(DebugUtilsMessageSeverityFlagsEXT.WarningBitExt))
{ {
@@ -378,34 +379,14 @@ namespace Ryujinx.Graphics.Vulkan
SType = StructureType.PhysicalDeviceFeatures2 SType = StructureType.PhysicalDeviceFeatures2
}; };
PhysicalDeviceVulkan11Features supportedFeaturesVk11 = new PhysicalDeviceVulkan11Features() PhysicalDeviceCustomBorderColorFeaturesEXT featuresCustomBorderColorSupported = new PhysicalDeviceCustomBorderColorFeaturesEXT()
{ {
SType = StructureType.PhysicalDeviceVulkan11Features, SType = StructureType.PhysicalDeviceCustomBorderColorFeaturesExt
PNext = features2.PNext
};
features2.PNext = &supportedFeaturesVk11;
PhysicalDeviceCustomBorderColorFeaturesEXT supportedFeaturesCustomBorderColor = new PhysicalDeviceCustomBorderColorFeaturesEXT()
{
SType = StructureType.PhysicalDeviceCustomBorderColorFeaturesExt,
PNext = features2.PNext
}; };
if (supportedExtensions.Contains("VK_EXT_custom_border_color")) if (supportedExtensions.Contains("VK_EXT_custom_border_color"))
{ {
features2.PNext = &supportedFeaturesCustomBorderColor; features2.PNext = &featuresCustomBorderColorSupported;
}
PhysicalDeviceTransformFeedbackFeaturesEXT supportedFeaturesTransformFeedback = new PhysicalDeviceTransformFeedbackFeaturesEXT()
{
SType = StructureType.PhysicalDeviceTransformFeedbackFeaturesExt,
PNext = features2.PNext
};
if (supportedExtensions.Contains(ExtTransformFeedback.ExtensionName))
{
features2.PNext = &supportedFeaturesTransformFeedback;
} }
PhysicalDeviceRobustness2FeaturesEXT supportedFeaturesRobustness2 = new PhysicalDeviceRobustness2FeaturesEXT() PhysicalDeviceRobustness2FeaturesEXT supportedFeaturesRobustness2 = new PhysicalDeviceRobustness2FeaturesEXT()
@@ -427,48 +408,41 @@ namespace Ryujinx.Graphics.Vulkan
var features = new PhysicalDeviceFeatures() var features = new PhysicalDeviceFeatures()
{ {
DepthBiasClamp = true, DepthBiasClamp = true,
DepthClamp = supportedFeatures.DepthClamp, DepthClamp = true,
DualSrcBlend = supportedFeatures.DualSrcBlend, DualSrcBlend = true,
FragmentStoresAndAtomics = true, FragmentStoresAndAtomics = true,
GeometryShader = supportedFeatures.GeometryShader, GeometryShader = supportedFeatures.GeometryShader,
ImageCubeArray = true, ImageCubeArray = true,
IndependentBlend = true, IndependentBlend = true,
LogicOp = supportedFeatures.LogicOp, LogicOp = supportedFeatures.LogicOp,
MultiViewport = supportedFeatures.MultiViewport, MultiViewport = true,
PipelineStatisticsQuery = supportedFeatures.PipelineStatisticsQuery, PipelineStatisticsQuery = supportedFeatures.PipelineStatisticsQuery,
SamplerAnisotropy = true, SamplerAnisotropy = true,
ShaderClipDistance = true, ShaderClipDistance = true,
ShaderFloat64 = supportedFeatures.ShaderFloat64, ShaderFloat64 = supportedFeatures.ShaderFloat64,
ShaderImageGatherExtended = supportedFeatures.ShaderImageGatherExtended, ShaderImageGatherExtended = true,
ShaderStorageImageMultisample = supportedFeatures.ShaderStorageImageMultisample, ShaderStorageImageMultisample = supportedFeatures.ShaderStorageImageMultisample,
// ShaderStorageImageReadWithoutFormat = true, // ShaderStorageImageReadWithoutFormat = true,
// ShaderStorageImageWriteWithoutFormat = true, // ShaderStorageImageWriteWithoutFormat = true,
TessellationShader = supportedFeatures.TessellationShader, TessellationShader = true,
VertexPipelineStoresAndAtomics = true, VertexPipelineStoresAndAtomics = true,
RobustBufferAccess = useRobustBufferAccess RobustBufferAccess = useRobustBufferAccess
}; };
void* pExtendedFeatures = null; void* pExtendedFeatures = null;
PhysicalDeviceTransformFeedbackFeaturesEXT featuresTransformFeedback; var featuresTransformFeedback = new PhysicalDeviceTransformFeedbackFeaturesEXT()
if (supportedExtensions.Contains(ExtTransformFeedback.ExtensionName))
{ {
featuresTransformFeedback = new PhysicalDeviceTransformFeedbackFeaturesEXT() SType = StructureType.PhysicalDeviceTransformFeedbackFeaturesExt,
{ PNext = pExtendedFeatures,
SType = StructureType.PhysicalDeviceTransformFeedbackFeaturesExt, TransformFeedback = true
PNext = pExtendedFeatures, };
TransformFeedback = supportedFeaturesTransformFeedback.TransformFeedback
};
pExtendedFeatures = &featuresTransformFeedback; pExtendedFeatures = &featuresTransformFeedback;
}
PhysicalDeviceRobustness2FeaturesEXT featuresRobustness2;
if (supportedExtensions.Contains("VK_EXT_robustness2")) if (supportedExtensions.Contains("VK_EXT_robustness2"))
{ {
featuresRobustness2 = new PhysicalDeviceRobustness2FeaturesEXT() var featuresRobustness2 = new PhysicalDeviceRobustness2FeaturesEXT()
{ {
SType = StructureType.PhysicalDeviceRobustness2FeaturesExt, SType = StructureType.PhysicalDeviceRobustness2FeaturesExt,
PNext = pExtendedFeatures, PNext = pExtendedFeatures,
@@ -491,7 +465,7 @@ namespace Ryujinx.Graphics.Vulkan
{ {
SType = StructureType.PhysicalDeviceVulkan11Features, SType = StructureType.PhysicalDeviceVulkan11Features,
PNext = pExtendedFeatures, PNext = pExtendedFeatures,
ShaderDrawParameters = supportedFeaturesVk11.ShaderDrawParameters ShaderDrawParameters = true
}; };
pExtendedFeatures = &featuresVk11; pExtendedFeatures = &featuresVk11;
@@ -552,8 +526,8 @@ namespace Ryujinx.Graphics.Vulkan
PhysicalDeviceCustomBorderColorFeaturesEXT featuresCustomBorderColor; PhysicalDeviceCustomBorderColorFeaturesEXT featuresCustomBorderColor;
if (supportedExtensions.Contains("VK_EXT_custom_border_color") && if (supportedExtensions.Contains("VK_EXT_custom_border_color") &&
supportedFeaturesCustomBorderColor.CustomBorderColors && featuresCustomBorderColorSupported.CustomBorderColors &&
supportedFeaturesCustomBorderColor.CustomBorderColorWithoutFormat) featuresCustomBorderColorSupported.CustomBorderColorWithoutFormat)
{ {
featuresCustomBorderColor = new PhysicalDeviceCustomBorderColorFeaturesEXT() featuresCustomBorderColor = new PhysicalDeviceCustomBorderColorFeaturesEXT()
{ {

View File

@@ -590,11 +590,7 @@ namespace Ryujinx.Graphics.Vulkan
IsAmdWindows = Vendor == Vendor.Amd && RuntimeInformation.IsOSPlatform(OSPlatform.Windows); IsAmdWindows = Vendor == Vendor.Amd && RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
IsIntelWindows = Vendor == Vendor.Intel && RuntimeInformation.IsOSPlatform(OSPlatform.Windows); IsIntelWindows = Vendor == Vendor.Intel && RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
IsTBDR = IsMoltenVk || IsTBDR = IsMoltenVk || Vendor == Vendor.Qualcomm || Vendor == Vendor.ARM || Vendor == Vendor.ImgTec;
Vendor == Vendor.Qualcomm ||
Vendor == Vendor.ARM ||
Vendor == Vendor.Broadcom ||
Vendor == Vendor.ImgTec;
GpuVendor = vendorName; GpuVendor = vendorName;
GpuRenderer = Marshal.PtrToStringAnsi((IntPtr)properties.DeviceName); GpuRenderer = Marshal.PtrToStringAnsi((IntPtr)properties.DeviceName);

View File

@@ -113,7 +113,7 @@ namespace Ryujinx.Graphics.Vulkan
ImageSharingMode = SharingMode.Exclusive, ImageSharingMode = SharingMode.Exclusive,
ImageArrayLayers = 1, ImageArrayLayers = 1,
PreTransform = capabilities.CurrentTransform, PreTransform = capabilities.CurrentTransform,
CompositeAlpha = ChooseCompositeAlpha(capabilities.SupportedCompositeAlpha), CompositeAlpha = CompositeAlphaFlagsKHR.OpaqueBitKhr,
PresentMode = ChooseSwapPresentMode(presentModes, _vsyncEnabled), PresentMode = ChooseSwapPresentMode(presentModes, _vsyncEnabled),
Clipped = true, Clipped = true,
OldSwapchain = oldSwapchain OldSwapchain = oldSwapchain
@@ -182,22 +182,6 @@ namespace Ryujinx.Graphics.Vulkan
return availableFormats[0]; return availableFormats[0];
} }
private static CompositeAlphaFlagsKHR ChooseCompositeAlpha(CompositeAlphaFlagsKHR supportedFlags)
{
if (supportedFlags.HasFlag(CompositeAlphaFlagsKHR.OpaqueBitKhr))
{
return CompositeAlphaFlagsKHR.OpaqueBitKhr;
}
else if (supportedFlags.HasFlag(CompositeAlphaFlagsKHR.PreMultipliedBitKhr))
{
return CompositeAlphaFlagsKHR.PreMultipliedBitKhr;
}
else
{
return CompositeAlphaFlagsKHR.InheritBitKhr;
}
}
private static PresentModeKHR ChooseSwapPresentMode(PresentModeKHR[] availablePresentModes, bool vsyncEnabled) private static PresentModeKHR ChooseSwapPresentMode(PresentModeKHR[] availablePresentModes, bool vsyncEnabled)
{ {
if (!vsyncEnabled && availablePresentModes.Contains(PresentModeKHR.ImmediateKhr)) if (!vsyncEnabled && availablePresentModes.Contains(PresentModeKHR.ImmediateKhr))
@@ -208,6 +192,10 @@ namespace Ryujinx.Graphics.Vulkan
{ {
return PresentModeKHR.MailboxKhr; return PresentModeKHR.MailboxKhr;
} }
else if (availablePresentModes.Contains(PresentModeKHR.FifoKhr))
{
return PresentModeKHR.FifoKhr;
}
else else
{ {
return PresentModeKHR.FifoKhr; return PresentModeKHR.FifoKhr;

View File

@@ -1,5 +1,4 @@
using Ryujinx.HLE.HOS.Kernel.Common; using Ryujinx.HLE.HOS.Kernel.Memory;
using Ryujinx.HLE.HOS.Kernel.Memory;
using Ryujinx.HLE.HOS.Kernel.Process; using Ryujinx.HLE.HOS.Kernel.Process;
using Ryujinx.HLE.HOS.Kernel.Threading; using Ryujinx.HLE.HOS.Kernel.Threading;
using Ryujinx.Horizon.Common; using Ryujinx.Horizon.Common;

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ namespace Ryujinx.Headless.SDL2
{ {
class SDL2MouseDriver : IGamepadDriver class SDL2MouseDriver : IGamepadDriver
{ {
private const int CursorHideIdleTime = 8; // seconds private const int CursorHideIdleTime = 5; // seconds
private bool _isDisposed; private bool _isDisposed;
private HideCursor _hideCursor; private HideCursor _hideCursor;

View File

@@ -435,12 +435,7 @@ namespace Ryujinx.Memory
public static ulong GetPageSize() public static ulong GetPageSize()
{ {
if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64) return (ulong)Environment.SystemPageSize;
{
return 1UL << 14;
}
return 1UL << 12;
} }
private static void ThrowInvalidMemoryRegionException() => throw new InvalidMemoryRegionException(); private static void ThrowInvalidMemoryRegionException() => throw new InvalidMemoryRegionException();

View File

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

View File

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

View File

@@ -68,7 +68,7 @@ namespace Ryujinx.Ui
private readonly CancellationTokenSource _gpuCancellationTokenSource; private readonly CancellationTokenSource _gpuCancellationTokenSource;
// Hide Cursor // Hide Cursor
const int CursorHideIdleTime = 8; // seconds const int CursorHideIdleTime = 5; // seconds
private static readonly Cursor _invisibleCursor = new Cursor(Display.Default, CursorType.BlankCursor); private static readonly Cursor _invisibleCursor = new Cursor(Display.Default, CursorType.BlankCursor);
private long _lastCursorMoveTime; private long _lastCursorMoveTime;
private bool _hideCursorOnIdle; private bool _hideCursorOnIdle;