Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
139a930407 | ||
|
719dc97bbd | ||
|
41bba5310a | ||
|
8071c8c8c0 | ||
|
b402b4e7f6 | ||
|
93df366b2c | ||
|
cd3a15aea5 | ||
|
070136b3f7 | ||
|
08ab47c6c0 | ||
|
85faa9d8fa |
@@ -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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
175
Ryujinx.Audio/Renderer/Dsp/UpsamplerHelper.cs
Normal file
175
Ryujinx.Audio/Renderer/Dsp/UpsamplerHelper.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
@@ -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>
|
||||||
|
@@ -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)
|
||||||
|
@@ -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:",
|
||||||
|
@@ -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()
|
||||||
|
@@ -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);
|
||||||
@@ -276,4 +276,4 @@ namespace Ryujinx.Ava
|
|||||||
Logger.Shutdown();
|
Logger.Shutdown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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);
|
||||||
|
|
||||||
|
@@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
226
Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs
Normal file
226
Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@@ -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">
|
||||||
|
@@ -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>
|
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -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>
|
||||||
|
@@ -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();
|
||||||
|
|
||||||
|
@@ -10,7 +10,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// A tracking handle for a texture group, which represents a range of views in a storage texture.
|
/// A tracking handle for a texture group, which represents a range of views in a storage texture.
|
||||||
/// Retains a list of overlapping texture views, a modified flag, and tracking for each
|
/// Retains a list of overlapping texture views, a modified flag, and tracking for each
|
||||||
/// CPU VA range that the views cover.
|
/// CPU VA range that the views cover.
|
||||||
/// Also tracks copy dependencies for the handle - references to other handles that must be kept
|
/// Also tracks copy dependencies for the handle - references to other handles that must be kept
|
||||||
/// in sync with this one before use.
|
/// in sync with this one before use.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class TextureGroupHandle : IDisposable
|
class TextureGroupHandle : IDisposable
|
||||||
@@ -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>
|
||||||
|
@@ -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)
|
||||||
{
|
{
|
||||||
|
@@ -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;
|
||||||
|
@@ -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()
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
@@ -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",
|
||||||
|
@@ -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()
|
||||||
{
|
{
|
||||||
|
@@ -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);
|
||||||
|
@@ -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;
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
using Ryujinx.HLE.HOS.Kernel.Common;
|
using Ryujinx.HLE.HOS.Kernel.Memory;
|
||||||
using Ryujinx.HLE.HOS.Kernel.Memory;
|
|
||||||
using Ryujinx.HLE.HOS.Kernel.Process;
|
using Ryujinx.HLE.HOS.Kernel.Process;
|
||||||
using Ryujinx.HLE.HOS.Kernel.Threading;
|
using Ryujinx.HLE.HOS.Kernel.Threading;
|
||||||
using Ryujinx.Horizon.Common;
|
using Ryujinx.Horizon.Common;
|
||||||
@@ -71,4 +70,4 @@ namespace Ryujinx.HLE.HOS.Kernel
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -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)
|
||||||
|
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
17
Ryujinx.HLE/HOS/Services/Pm/ResultCode.cs
Normal file
17
Ryujinx.HLE/HOS/Services/Pm/ResultCode.cs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
@@ -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;
|
||||||
|
@@ -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();
|
||||||
|
@@ -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)
|
||||||
|
@@ -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;
|
||||||
|
Reference in New Issue
Block a user