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]);
|
||||
}
|
||||
|
||||
if (info.BufferStates?.Length != (int)inputCount)
|
||||
{
|
||||
// Keep state if possible.
|
||||
info.BufferStates = new UpsamplerBufferState[(int)inputCount];
|
||||
}
|
||||
|
||||
UpsamplerInfo = info;
|
||||
}
|
||||
|
||||
@@ -50,8 +56,6 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public void Process(CommandList context)
|
||||
{
|
||||
float ratio = (float)InputSampleRate / Constants.TargetSampleRate;
|
||||
|
||||
uint bufferCount = Math.Min(BufferCount, UpsamplerInfo.SourceSampleCount);
|
||||
|
||||
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> outputBuffer = GetBuffer(UpsamplerInfo.InputBufferIndices[i], (int)UpsamplerInfo.SampleCount);
|
||||
|
||||
float fraction = 0.0f;
|
||||
|
||||
ResamplerHelper.ResampleForUpsampler(outputBuffer, inputBuffer, ratio, ref fraction, (int)(InputSampleCount / ratio));
|
||||
UpsamplerHelper.Upsample(outputBuffer, inputBuffer, (int)UpsamplerInfo.SampleCount, (int)InputSampleCount, ref UpsamplerInfo.BufferStates[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -579,52 +579,5 @@ namespace Ryujinx.Audio.Renderer.Dsp
|
||||
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>
|
||||
public ushort[] InputBufferIndices;
|
||||
|
||||
/// <summary>
|
||||
/// State of each input buffer index kept across invocations of the upsampler.
|
||||
/// </summary>
|
||||
public UpsamplerBufferState[] BufferStates;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="UpsamplerState"/>.
|
||||
/// </summary>
|
||||
|
@@ -44,8 +44,10 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop;
|
||||
using Image = SixLabors.ImageSharp.Image;
|
||||
using InputManager = Ryujinx.Input.HLE.InputManager;
|
||||
using Key = Ryujinx.Input.Key;
|
||||
@@ -58,12 +60,14 @@ namespace Ryujinx.Ava
|
||||
{
|
||||
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 int TargetFps = 60;
|
||||
private const float VolumeDelta = 0.05f;
|
||||
|
||||
private static readonly Cursor InvisibleCursor = new(StandardCursorType.None);
|
||||
private readonly IntPtr InvisibleCursorWin;
|
||||
private readonly IntPtr DefaultCursorWin;
|
||||
|
||||
private readonly long _ticksPerFrame;
|
||||
private readonly Stopwatch _chrono;
|
||||
@@ -81,7 +85,6 @@ namespace Ryujinx.Ava
|
||||
private float _newVolume;
|
||||
private KeyboardHotkeyState _prevHotkeyState;
|
||||
|
||||
private bool _hideCursorOnIdle;
|
||||
private long _lastCursorMoveTime;
|
||||
private bool _isCursorInRenderer;
|
||||
|
||||
@@ -131,7 +134,6 @@ namespace Ryujinx.Ava
|
||||
_accountManager = accountManager;
|
||||
_userChannelPersistence = userChannelPersistence;
|
||||
_renderingThread = new Thread(RenderLoop, 1 * 1024 * 1024) { Name = "GUI.RenderThread" };
|
||||
_hideCursorOnIdle = ConfigurationState.Instance.HideCursorOnIdle;
|
||||
_lastCursorMoveTime = Stopwatch.GetTimestamp();
|
||||
_glLogLevel = ConfigurationState.Instance.Logger.GraphicsDebugLevel;
|
||||
_topLevel = topLevel;
|
||||
@@ -159,9 +161,14 @@ namespace Ryujinx.Ava
|
||||
|
||||
ConfigurationState.Instance.HideCursorOnIdle.Event += HideCursorState_Changed;
|
||||
|
||||
_topLevel.PointerLeave += TopLevel_PointerLeave;
|
||||
_topLevel.PointerMoved += TopLevel_PointerMoved;
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
InvisibleCursorWin = CreateEmptyCursor();
|
||||
DefaultCursorWin = CreateArrowCursor();
|
||||
}
|
||||
|
||||
ConfigurationState.Instance.System.IgnoreMissingServices.Event += UpdateIgnoreMissingServicesState;
|
||||
ConfigurationState.Instance.Graphics.AspectRatio.Event += UpdateAspectRatioState;
|
||||
ConfigurationState.Instance.System.EnableDockedMode.Event += UpdateDockedModeState;
|
||||
@@ -172,20 +179,47 @@ namespace Ryujinx.Ava
|
||||
|
||||
private void TopLevel_PointerMoved(object sender, PointerEventArgs e)
|
||||
{
|
||||
if (sender is Control visual)
|
||||
if (sender is MainWindow window)
|
||||
{
|
||||
_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;
|
||||
_viewModel.Cursor = Cursor.Default;
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
_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)
|
||||
@@ -380,7 +414,6 @@ namespace Ryujinx.Ava
|
||||
ConfigurationState.Instance.System.EnableDockedMode.Event -= UpdateDockedModeState;
|
||||
ConfigurationState.Instance.System.AudioVolume.Event -= UpdateAudioVolumeState;
|
||||
|
||||
_topLevel.PointerLeave -= TopLevel_PointerLeave;
|
||||
_topLevel.PointerMoved -= TopLevel_PointerMoved;
|
||||
|
||||
_gpuCancellationTokenSource.Cancel();
|
||||
@@ -406,19 +439,10 @@ namespace Ryujinx.Ava
|
||||
|
||||
private void HideCursorState_Changed(object sender, ReactiveEventArgs<bool> state)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(delegate
|
||||
if (state.NewValue)
|
||||
{
|
||||
_hideCursorOnIdle = state.NewValue;
|
||||
|
||||
if (_hideCursorOnIdle)
|
||||
{
|
||||
_lastCursorMoveTime = Stopwatch.GetTimestamp();
|
||||
}
|
||||
else
|
||||
{
|
||||
_viewModel.Cursor = Cursor.Default;
|
||||
}
|
||||
});
|
||||
_lastCursorMoveTime = Stopwatch.GetTimestamp();
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
if (!_isActive)
|
||||
@@ -890,23 +891,44 @@ namespace Ryujinx.Ava
|
||||
return false;
|
||||
}
|
||||
|
||||
NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
|
||||
|
||||
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(() =>
|
||||
{
|
||||
HandleScreenState();
|
||||
|
||||
if (_keyboardInterface.GetKeyboardStateSnapshot().IsPressed(Key.Delete) && _viewModel.WindowState != WindowState.FullScreen)
|
||||
{
|
||||
Device.Application.DiskCacheLoadState?.Cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
|
||||
|
||||
if (_viewModel.IsActive)
|
||||
{
|
||||
KeyboardHotkeyState currentHotkeyState = GetHotkeyState();
|
||||
|
||||
if (currentHotkeyState != _prevHotkeyState)
|
||||
|
@@ -524,7 +524,7 @@
|
||||
"UserErrorUndefinedDescription": "An undefined error occured! This shouldn't happen, please contact a dev!",
|
||||
"OpenSetupGuideMessage": "Open the Setup Guide",
|
||||
"NoUpdate": "No Update",
|
||||
"TitleUpdateVersionLabel": "Version {0} - {1}",
|
||||
"TitleUpdateVersionLabel": "Version {0}",
|
||||
"RyujinxInfo": "Ryujinx - Info",
|
||||
"RyujinxConfirm": "Ryujinx - Confirmation",
|
||||
"FileDialogAllTypes": "All types",
|
||||
@@ -585,7 +585,7 @@
|
||||
"UserProfilesSetProfileImage": "Set Profile Image",
|
||||
"UserProfileEmptyNameError": "Name is required",
|
||||
"UserProfileNoImageError": "Profile image must be set",
|
||||
"GameUpdateWindowHeading": "{0} Update(s) available for {1} ({2})",
|
||||
"GameUpdateWindowHeading": "Manage Updates for {0} ({1})",
|
||||
"SettingsTabHotkeysResScaleUpHotkey": "Increase resolution:",
|
||||
"SettingsTabHotkeysResScaleDownHotkey": "Decrease resolution:",
|
||||
"UserProfilesName": "Name:",
|
||||
|
@@ -1,6 +1,7 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using FluentAvalonia.Core;
|
||||
using Ryujinx.Input;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
@@ -69,12 +70,22 @@ namespace Ryujinx.Ava.Input
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
@@ -86,12 +97,18 @@ namespace Ryujinx.Ava.Input
|
||||
|
||||
public void SetMousePressed(MouseButton button)
|
||||
{
|
||||
PressedButtons[(int)button] = true;
|
||||
if (PressedButtons.Count() >= (int)button)
|
||||
{
|
||||
PressedButtons[(int)button] = true;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -101,7 +118,12 @@ namespace Ryujinx.Ava.Input
|
||||
|
||||
public bool IsButtonPressed(MouseButton button)
|
||||
{
|
||||
return PressedButtons[(int)button];
|
||||
if (PressedButtons.Count() >= (int)button)
|
||||
{
|
||||
return PressedButtons[(int)button];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public Size GetClientSize()
|
||||
|
@@ -28,7 +28,7 @@ namespace Ryujinx.Ava
|
||||
public static double DesktopScaleFactor { get; set; } = 1.0;
|
||||
public static string Version { 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)]
|
||||
public static partial int MessageBoxA(IntPtr hWnd, [MarshalAs(UnmanagedType.LPStr)] string text, [MarshalAs(UnmanagedType.LPStr)] string caption, uint type);
|
||||
|
@@ -34,6 +34,8 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
WindowHandle = IntPtr.Zero;
|
||||
X11Display = IntPtr.Zero;
|
||||
NsView = IntPtr.Zero;
|
||||
MetalLayer = IntPtr.Zero;
|
||||
}
|
||||
|
||||
public EmbeddedWindow()
|
||||
@@ -42,7 +44,7 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
|
||||
stateObserverable.Subscribe(StateChanged);
|
||||
|
||||
this.Initialized += NativeEmbeddedWindow_Initialized;
|
||||
Initialized += NativeEmbeddedWindow_Initialized;
|
||||
}
|
||||
|
||||
public virtual void OnWindowCreated() { }
|
||||
@@ -127,7 +129,7 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
lpfnWndProc = Marshal.GetFunctionPointerForDelegate(_wndProcDelegate),
|
||||
style = ClassStyles.CS_OWNDC,
|
||||
lpszClassName = Marshal.StringToHGlobalUni(_className),
|
||||
hCursor = LoadCursor(IntPtr.Zero, (IntPtr)Cursors.IDC_ARROW)
|
||||
hCursor = CreateArrowCursor()
|
||||
};
|
||||
|
||||
var atom = RegisterClassEx(ref wndClassEx);
|
||||
@@ -198,6 +200,7 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
KeyModifiers.None));
|
||||
break;
|
||||
}
|
||||
|
||||
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")]
|
||||
public static partial ushort RegisterClassEx(ref WNDCLASSEX param);
|
||||
|
||||
|
@@ -3,23 +3,17 @@ using Ryujinx.Ava.Common.Locale;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Models
|
||||
{
|
||||
internal class TitleUpdateModel
|
||||
public class TitleUpdateModel
|
||||
{
|
||||
public bool IsEnabled { get; set; }
|
||||
public bool IsNoUpdate { get; }
|
||||
public ApplicationControlProperty Control { get; }
|
||||
public string Path { get; }
|
||||
|
||||
public string Label => IsNoUpdate
|
||||
? LocaleManager.Instance[LocaleKeys.NoUpdate]
|
||||
: string.Format(LocaleManager.Instance[LocaleKeys.TitleUpdateVersionLabel], Control.DisplayVersionString.ToString(),
|
||||
Path);
|
||||
public string Label => string.Format(LocaleManager.Instance[LocaleKeys.TitleUpdateVersionLabel], Control.DisplayVersionString.ToString());
|
||||
|
||||
public TitleUpdateModel(ApplicationControlProperty control, string path, bool isNoUpdate = false)
|
||||
public TitleUpdateModel(ApplicationControlProperty control, string path)
|
||||
{
|
||||
Control = control;
|
||||
Path = path;
|
||||
IsNoUpdate = isNoUpdate;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1601,13 +1601,9 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
public async void OpenTitleUpdateManager()
|
||||
{
|
||||
ApplicationData selection = SelectedApplication;
|
||||
if (selection != null)
|
||||
if (SelectedApplication != null)
|
||||
{
|
||||
if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
await new TitleUpdateWindow(VirtualFileSystem, ulong.Parse(selection.TitleId, NumberStyles.HexNumber), selection.TitleName).ShowDialog(desktop.MainWindow);
|
||||
}
|
||||
await TitleUpdateWindow.Show(VirtualFileSystem, ulong.Parse(SelectedApplication.TitleId, NumberStyles.HexNumber), SelectedApplication.TitleName);
|
||||
}
|
||||
}
|
||||
|
||||
|
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">
|
||||
<ListBox
|
||||
Name="SaveList"
|
||||
VirtualizationMode="None"
|
||||
Items="{Binding Views}"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
@@ -116,6 +117,9 @@
|
||||
<Setter Property="Margin" Value="5" />
|
||||
<Setter Property="CornerRadius" Value="4" />
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem:selected /template/ Border#SelectionIndicator">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:SaveModel">
|
||||
|
@@ -1,115 +1,135 @@
|
||||
<window:StyleableWindow
|
||||
<UserControl
|
||||
x:Class="Ryujinx.Ava.UI.Windows.TitleUpdateWindow"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows"
|
||||
Width="600"
|
||||
Height="400"
|
||||
MinWidth="600"
|
||||
MinHeight="400"
|
||||
MaxWidth="600"
|
||||
MaxHeight="400"
|
||||
SizeToContent="Height"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
Width="500"
|
||||
Height="300"
|
||||
mc:Ignorable="d"
|
||||
x:CompileBindings="True"
|
||||
x:DataType="viewModels:TitleUpdateViewModel"
|
||||
Focusable="True">
|
||||
<Grid Margin="15">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock
|
||||
Name="Heading"
|
||||
Grid.Row="1"
|
||||
MaxWidth="500"
|
||||
Margin="20,15,20,20"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
LineHeight="18"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
<Border
|
||||
Grid.Row="2"
|
||||
Margin="5"
|
||||
Grid.Row="0"
|
||||
Margin="0 0 0 24"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1">
|
||||
<ScrollViewer
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<ItemsControl
|
||||
Margin="10"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Items="{Binding _titleUpdates}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<RadioButton
|
||||
Padding="8,0"
|
||||
VerticalContentAlignment="Center"
|
||||
GroupName="Update"
|
||||
IsChecked="{Binding IsEnabled, Mode=TwoWay}">
|
||||
<Label
|
||||
Margin="0"
|
||||
BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5"
|
||||
Padding="2.5">
|
||||
<ListBox
|
||||
VirtualizationMode="None"
|
||||
Background="Transparent"
|
||||
SelectedItem="{Binding SelectedUpdate, Mode=TwoWay}"
|
||||
Items="{Binding Views}">
|
||||
<ListBox.DataTemplates>
|
||||
<DataTemplate
|
||||
DataType="models:TitleUpdateModel">
|
||||
<Panel Margin="10">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
TextWrapping="Wrap"
|
||||
Text="{Binding Label}" />
|
||||
<StackPanel
|
||||
Spacing="10"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right">
|
||||
<Button
|
||||
VerticalAlignment="Center"
|
||||
Content="{Binding Label}"
|
||||
FontSize="12" />
|
||||
</RadioButton>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
HorizontalAlignment="Right"
|
||||
Padding="10"
|
||||
MinWidth="0"
|
||||
MinHeight="0"
|
||||
Click="OpenLocation">
|
||||
<ui:SymbolIcon
|
||||
Symbol="OpenFolder"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Button>
|
||||
<Button
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Right"
|
||||
Padding="10"
|
||||
MinWidth="0"
|
||||
MinHeight="0"
|
||||
Click="RemoveUpdate">
|
||||
<ui:SymbolIcon
|
||||
Symbol="Cancel"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
</DataTemplate>
|
||||
<DataTemplate
|
||||
DataType="viewModels:BaseModel">
|
||||
<Panel
|
||||
Height="33"
|
||||
Margin="10">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
TextWrapping="Wrap"
|
||||
Text="{locale:Locale NoUpdate}" />
|
||||
</Panel>
|
||||
</DataTemplate>
|
||||
</ListBox.DataTemplates>
|
||||
<ListBox.Styles>
|
||||
<Style Selector="ListBoxItem">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
</ListBox>
|
||||
</Border>
|
||||
<DockPanel
|
||||
Grid.Row="3"
|
||||
Margin="0"
|
||||
<Panel
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch">
|
||||
<DockPanel Margin="0" HorizontalAlignment="Left">
|
||||
<StackPanel
|
||||
Orientation="Horizontal"
|
||||
Spacing="10"
|
||||
HorizontalAlignment="Left">
|
||||
<Button
|
||||
Name="AddButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding Add}">
|
||||
Command="{ReflectionBinding Add}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabGeneralAdd}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="RemoveButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding RemoveSelected}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabGeneralRemove}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="RemoveAllButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding RemoveAll}">
|
||||
Click="RemoveAll">
|
||||
<TextBlock Text="{locale:Locale DlcManagerRemoveAllButton}" />
|
||||
</Button>
|
||||
</DockPanel>
|
||||
<DockPanel Margin="0" HorizontalAlignment="Right">
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Orientation="Horizontal"
|
||||
Spacing="10"
|
||||
HorizontalAlignment="Right">
|
||||
<Button
|
||||
Name="SaveButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding Save}">
|
||||
Click="Save">
|
||||
<TextBlock Text="{locale:Locale SettingsButtonSave}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="CancelButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding Close}">
|
||||
Click="Close">
|
||||
<TextBlock Text="{locale:Locale InputDialogCancel}" />
|
||||
</Button>
|
||||
</DockPanel>
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
</Grid>
|
||||
</window:StyleableWindow>
|
||||
</UserControl>
|
@@ -1,271 +1,116 @@
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Threading;
|
||||
using LibHac.Common;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
using LibHac.FsSystem;
|
||||
using LibHac.Ns;
|
||||
using LibHac.Tools.FsSystem;
|
||||
using LibHac.Tools.FsSystem.NcaUtils;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Styling;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.HOS;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Path = System.IO.Path;
|
||||
using SpanHelpers = LibHac.Common.SpanHelpers;
|
||||
using System.Threading.Tasks;
|
||||
using Button = Avalonia.Controls.Button;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Windows
|
||||
{
|
||||
public partial class TitleUpdateWindow : StyleableWindow
|
||||
public partial class TitleUpdateWindow : UserControl
|
||||
{
|
||||
private readonly string _titleUpdateJsonPath;
|
||||
private TitleUpdateMetadata _titleUpdateWindowData;
|
||||
|
||||
private VirtualFileSystem _virtualFileSystem { get; }
|
||||
private AvaloniaList<TitleUpdateModel> _titleUpdates { get; set; }
|
||||
|
||||
private ulong _titleId { get; }
|
||||
private string _titleName { get; }
|
||||
public TitleUpdateViewModel ViewModel;
|
||||
|
||||
public TitleUpdateWindow()
|
||||
{
|
||||
DataContext = this;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance[LocaleKeys.UpdateWindowTitle]} - {_titleName} ({_titleId:X16})";
|
||||
}
|
||||
|
||||
public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
|
||||
{
|
||||
_virtualFileSystem = virtualFileSystem;
|
||||
_titleUpdates = new AvaloniaList<TitleUpdateModel>();
|
||||
|
||||
_titleId = titleId;
|
||||
_titleName = titleName;
|
||||
|
||||
_titleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "updates.json");
|
||||
|
||||
try
|
||||
{
|
||||
_titleUpdateWindowData = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(_titleUpdateJsonPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_titleUpdateWindowData = new TitleUpdateMetadata
|
||||
{
|
||||
Selected = "",
|
||||
Paths = new List<string>()
|
||||
};
|
||||
}
|
||||
|
||||
DataContext = this;
|
||||
DataContext = ViewModel = new TitleUpdateViewModel(virtualFileSystem, titleId, titleName);
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance[LocaleKeys.UpdateWindowTitle]} - {_titleName} ({_titleId:X16})";
|
||||
|
||||
LoadUpdates();
|
||||
PrintHeading();
|
||||
}
|
||||
|
||||
private void PrintHeading()
|
||||
public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
|
||||
{
|
||||
Heading.Text = string.Format(LocaleManager.Instance[LocaleKeys.GameUpdateWindowHeading], _titleUpdates.Count - 1, _titleName, _titleId.ToString("X16"));
|
||||
}
|
||||
|
||||
private void LoadUpdates()
|
||||
{
|
||||
_titleUpdates.Add(new TitleUpdateModel(default, string.Empty, true));
|
||||
|
||||
foreach (string path in _titleUpdateWindowData.Paths)
|
||||
ContentDialog contentDialog = new()
|
||||
{
|
||||
AddUpdate(path);
|
||||
}
|
||||
|
||||
if (_titleUpdateWindowData.Selected == "")
|
||||
{
|
||||
_titleUpdates[0].IsEnabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
TitleUpdateModel selected = _titleUpdates.FirstOrDefault(x => x.Path == _titleUpdateWindowData.Selected);
|
||||
List<TitleUpdateModel> enabled = _titleUpdates.Where(x => x.IsEnabled).ToList();
|
||||
|
||||
foreach (TitleUpdateModel update in enabled)
|
||||
{
|
||||
update.IsEnabled = false;
|
||||
}
|
||||
|
||||
if (selected != null)
|
||||
{
|
||||
selected.IsEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
SortUpdates();
|
||||
}
|
||||
|
||||
private void AddUpdate(string path)
|
||||
{
|
||||
if (File.Exists(path) && !_titleUpdates.Any(x => x.Path == path))
|
||||
{
|
||||
using FileStream file = new(path, FileMode.Open, FileAccess.Read);
|
||||
|
||||
try
|
||||
{
|
||||
(Nca patchNca, Nca controlNca) = ApplicationLoader.GetGameUpdateDataFromPartition(_virtualFileSystem, new PartitionFileSystem(file.AsStorage()), _titleId.ToString("x16"), 0);
|
||||
|
||||
if (controlNca != null && patchNca != null)
|
||||
{
|
||||
ApplicationControlProperty controlData = new();
|
||||
|
||||
using UniqueRef<IFile> nacpFile = new();
|
||||
|
||||
controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref(), "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure();
|
||||
|
||||
_titleUpdates.Add(new TitleUpdateModel(controlData, path));
|
||||
|
||||
foreach (var update in _titleUpdates)
|
||||
{
|
||||
update.IsEnabled = false;
|
||||
}
|
||||
|
||||
_titleUpdates.Last().IsEnabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Dispatcher.UIThread.Post(async () =>
|
||||
{
|
||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]);
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Dispatcher.UIThread.Post(async () =>
|
||||
{
|
||||
await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[LocaleKeys.DialogDlcLoadNcaErrorMessage], ex.Message, path));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveUpdates(bool removeSelectedOnly = false)
|
||||
{
|
||||
if (removeSelectedOnly)
|
||||
{
|
||||
_titleUpdates.RemoveAll(_titleUpdates.Where(x => x.IsEnabled && !x.IsNoUpdate).ToList());
|
||||
}
|
||||
else
|
||||
{
|
||||
_titleUpdates.RemoveAll(_titleUpdates.Where(x => !x.IsNoUpdate).ToList());
|
||||
}
|
||||
|
||||
_titleUpdates.FirstOrDefault(x => x.IsNoUpdate).IsEnabled = true;
|
||||
|
||||
SortUpdates();
|
||||
PrintHeading();
|
||||
}
|
||||
|
||||
public void RemoveSelected()
|
||||
{
|
||||
RemoveUpdates(true);
|
||||
}
|
||||
|
||||
public void RemoveAll()
|
||||
{
|
||||
RemoveUpdates();
|
||||
}
|
||||
|
||||
public async void Add()
|
||||
{
|
||||
OpenFileDialog dialog = new()
|
||||
{
|
||||
Title = LocaleManager.Instance[LocaleKeys.SelectUpdateDialogTitle],
|
||||
AllowMultiple = true
|
||||
PrimaryButtonText = "",
|
||||
SecondaryButtonText = "",
|
||||
CloseButtonText = "",
|
||||
Content = new TitleUpdateWindow(virtualFileSystem, titleId, titleName),
|
||||
Title = string.Format(LocaleManager.Instance[LocaleKeys.GameUpdateWindowHeading], titleName, titleId.ToString("X16"))
|
||||
};
|
||||
|
||||
dialog.Filters.Add(new FileDialogFilter
|
||||
{
|
||||
Name = "NSP",
|
||||
Extensions = { "nsp" }
|
||||
});
|
||||
Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>());
|
||||
bottomBorder.Setters.Add(new Setter(IsVisibleProperty, false));
|
||||
|
||||
string[] files = await dialog.ShowAsync(this);
|
||||
contentDialog.Styles.Add(bottomBorder);
|
||||
|
||||
if (files != null)
|
||||
{
|
||||
foreach (string file in files)
|
||||
{
|
||||
AddUpdate(file);
|
||||
}
|
||||
}
|
||||
|
||||
SortUpdates();
|
||||
PrintHeading();
|
||||
await ContentDialogHelper.ShowAsync(contentDialog);
|
||||
}
|
||||
|
||||
private void SortUpdates()
|
||||
private void Close(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var list = _titleUpdates.ToList();
|
||||
|
||||
list.Sort((first, second) =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(first.Control.DisplayVersionString.ToString()))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
else if (string.IsNullOrEmpty(second.Control.DisplayVersionString.ToString()))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
return Version.Parse(first.Control.DisplayVersionString.ToString()).CompareTo(Version.Parse(second.Control.DisplayVersionString.ToString())) * -1;
|
||||
});
|
||||
|
||||
_titleUpdates.Clear();
|
||||
_titleUpdates.AddRange(list);
|
||||
((ContentDialog)Parent).Hide();
|
||||
}
|
||||
|
||||
public void Save()
|
||||
public void Save(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_titleUpdateWindowData.Paths.Clear();
|
||||
ViewModel._titleUpdateWindowData.Paths.Clear();
|
||||
|
||||
_titleUpdateWindowData.Selected = "";
|
||||
ViewModel._titleUpdateWindowData.Selected = "";
|
||||
|
||||
foreach (TitleUpdateModel update in _titleUpdates)
|
||||
foreach (TitleUpdateModel update in ViewModel.TitleUpdates)
|
||||
{
|
||||
_titleUpdateWindowData.Paths.Add(update.Path);
|
||||
ViewModel._titleUpdateWindowData.Paths.Add(update.Path);
|
||||
|
||||
if (update.IsEnabled)
|
||||
if (update == ViewModel.SelectedUpdate)
|
||||
{
|
||||
_titleUpdateWindowData.Selected = update.Path;
|
||||
ViewModel._titleUpdateWindowData.Selected = update.Path;
|
||||
}
|
||||
}
|
||||
|
||||
using (FileStream titleUpdateJsonStream = File.Create(_titleUpdateJsonPath, 4096, FileOptions.WriteThrough))
|
||||
using (FileStream titleUpdateJsonStream = File.Create(ViewModel._titleUpdateJsonPath, 4096, FileOptions.WriteThrough))
|
||||
{
|
||||
titleUpdateJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_titleUpdateWindowData, true)));
|
||||
titleUpdateJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(ViewModel._titleUpdateWindowData, true)));
|
||||
}
|
||||
|
||||
if (Owner is MainWindow window)
|
||||
if (VisualRoot is MainWindow window)
|
||||
{
|
||||
window.ViewModel.LoadApplications();
|
||||
}
|
||||
|
||||
Close();
|
||||
((ContentDialog)Parent).Hide();
|
||||
}
|
||||
|
||||
private void OpenLocation(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button button)
|
||||
{
|
||||
if (button.DataContext is TitleUpdateModel model)
|
||||
{
|
||||
OpenHelper.LocateFile(model.Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveUpdate(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button button)
|
||||
{
|
||||
ViewModel.RemoveUpdate((TitleUpdateModel)button.DataContext);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveAll(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.TitleUpdates.Clear();
|
||||
|
||||
ViewModel.SortUpdates();
|
||||
}
|
||||
}
|
||||
}
|
@@ -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.
|
||||
TextureManager.ReloadPools();
|
||||
MemoryManager.Physical.BufferCache.QueuePrune();
|
||||
memoryManager.Physical.BufferCache.QueuePrune();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -84,7 +84,9 @@ namespace Ryujinx.Graphics.Gpu
|
||||
private void MemoryUnmappedHandler(object sender, UnmapEventArgs e)
|
||||
{
|
||||
TextureManager.ReloadPools();
|
||||
MemoryManager.Physical.BufferCache.QueuePrune();
|
||||
|
||||
var memoryManager = Volatile.Read(ref _memoryManager);
|
||||
memoryManager?.Physical.BufferCache.QueuePrune();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@@ -1431,9 +1431,20 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
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(() =>
|
||||
{
|
||||
handle.Sync(_context);
|
||||
if (!isGpuThread)
|
||||
{
|
||||
handle.Sync(_context);
|
||||
}
|
||||
|
||||
Storage.SignalModifiedDirty();
|
||||
|
||||
|
@@ -232,32 +232,23 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// <param name="context">The GPU context used to wait for sync</param>
|
||||
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;
|
||||
long diff = (long)(context.SyncNumber - registeredSync);
|
||||
context.Renderer.WaitSync(registeredSync);
|
||||
|
||||
if (diff > 0)
|
||||
if ((long)(_modifiedSync - registeredSync) > 0)
|
||||
{
|
||||
context.Renderer.WaitSync(registeredSync);
|
||||
|
||||
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;
|
||||
// Flush the data in a previous state. Do not remove the modified flag - it will be removed to ignore following writes.
|
||||
return;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// If the difference is <= 0, no data is not ready yet. Flush any data we can without waiting or removing modified flag.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@@ -14,12 +14,6 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
MemoryPropertyFlags.HostCoherentBit |
|
||||
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 =
|
||||
MemoryPropertyFlags.DeviceLocalBit;
|
||||
|
||||
@@ -100,21 +94,9 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
gd.Api.CreateBuffer(_device, in bufferCreateInfo, null, out var buffer).ThrowOnError();
|
||||
gd.Api.GetBufferMemoryRequirements(_device, buffer, out var requirements);
|
||||
|
||||
MemoryPropertyFlags allocateFlags;
|
||||
MemoryPropertyFlags allocateFlagsAlt;
|
||||
var allocateFlags = deviceLocal ? DeviceLocalBufferMemoryFlags : DefaultBufferMemoryFlags;
|
||||
|
||||
if (deviceLocal)
|
||||
{
|
||||
allocateFlags = DeviceLocalBufferMemoryFlags;
|
||||
allocateFlagsAlt = DeviceLocalBufferMemoryFlags;
|
||||
}
|
||||
else
|
||||
{
|
||||
allocateFlags = DefaultBufferMemoryFlags;
|
||||
allocateFlagsAlt = DefaultBufferMemoryAltFlags;
|
||||
}
|
||||
|
||||
var allocation = gd.MemoryAllocator.AllocateDeviceMemory(_physicalDevice, requirements, allocateFlags, allocateFlagsAlt);
|
||||
var allocation = gd.MemoryAllocator.AllocateDeviceMemory(_physicalDevice, requirements, allocateFlags);
|
||||
|
||||
if (allocation.Memory.Handle == 0UL)
|
||||
{
|
||||
|
@@ -23,7 +23,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
public int[] AttachmentIndices { 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 int ColorAttachmentsCount => AttachmentsCount - (HasDepthStencil ? 1 : 0);
|
||||
|
||||
@@ -67,6 +67,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
AttachmentSamples = new uint[count];
|
||||
AttachmentFormats = new VkFormat[count];
|
||||
AttachmentIndices = new int[count];
|
||||
MaxColorAttachmentIndex = colors.Length - 1;
|
||||
|
||||
uint width = uint.MaxValue;
|
||||
uint height = uint.MaxValue;
|
||||
|
@@ -27,16 +27,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
MemoryRequirements requirements,
|
||||
MemoryPropertyFlags flags = 0)
|
||||
{
|
||||
return AllocateDeviceMemory(physicalDevice, requirements, flags, flags);
|
||||
}
|
||||
|
||||
public MemoryAllocation AllocateDeviceMemory(
|
||||
PhysicalDevice physicalDevice,
|
||||
MemoryRequirements requirements,
|
||||
MemoryPropertyFlags flags,
|
||||
MemoryPropertyFlags alternativeFlags)
|
||||
{
|
||||
int memoryTypeIndex = FindSuitableMemoryTypeIndex(_api, physicalDevice, requirements.MemoryTypeBits, flags, alternativeFlags);
|
||||
int memoryTypeIndex = FindSuitableMemoryTypeIndex(_api, physicalDevice, requirements.MemoryTypeBits, flags);
|
||||
if (memoryTypeIndex < 0)
|
||||
{
|
||||
return default;
|
||||
@@ -65,35 +56,21 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
return newBl.Allocate(size, alignment, map);
|
||||
}
|
||||
|
||||
private static int FindSuitableMemoryTypeIndex(
|
||||
Vk api,
|
||||
PhysicalDevice physicalDevice,
|
||||
uint memoryTypeBits,
|
||||
MemoryPropertyFlags flags,
|
||||
MemoryPropertyFlags alternativeFlags)
|
||||
private static int FindSuitableMemoryTypeIndex(Vk api, PhysicalDevice physicalDevice, uint memoryTypeBits, MemoryPropertyFlags flags)
|
||||
{
|
||||
int bestCandidateIndex = -1;
|
||||
|
||||
api.GetPhysicalDeviceMemoryProperties(physicalDevice, out var properties);
|
||||
|
||||
for (int i = 0; i < properties.MemoryTypeCount; 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;
|
||||
}
|
||||
else if (type.PropertyFlags.HasFlag(alternativeFlags))
|
||||
{
|
||||
bestCandidateIndex = i;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return bestCandidateIndex;
|
||||
return -1;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
@@ -1344,7 +1344,8 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
var dstAttachmentFormats = _newState.Internal.AttachmentFormats.AsSpan();
|
||||
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;
|
||||
}
|
||||
|
@@ -9,7 +9,6 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
Intel,
|
||||
Nvidia,
|
||||
ARM,
|
||||
Broadcom,
|
||||
Qualcomm,
|
||||
Apple,
|
||||
Unknown
|
||||
@@ -29,7 +28,6 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
0x106B => Vendor.Apple,
|
||||
0x10DE => Vendor.Nvidia,
|
||||
0x13B5 => Vendor.ARM,
|
||||
0x14E4 => Vendor.Broadcom,
|
||||
0x8086 => Vendor.Intel,
|
||||
0x5143 => Vendor.Qualcomm,
|
||||
_ => Vendor.Unknown
|
||||
@@ -45,7 +43,6 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
0x106B => "Apple",
|
||||
0x10DE => "NVIDIA",
|
||||
0x13B5 => "ARM",
|
||||
0x14E4 => "Broadcom",
|
||||
0x1AE0 => "Google",
|
||||
0x5143 => "Qualcomm",
|
||||
0x8086 => "Intel",
|
||||
|
@@ -162,6 +162,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
if (messageSeverity.HasFlag(DebugUtilsMessageSeverityFlagsEXT.ErrorBitExt))
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Gpu, msg);
|
||||
//throw new Exception(msg);
|
||||
}
|
||||
else if (messageSeverity.HasFlag(DebugUtilsMessageSeverityFlagsEXT.WarningBitExt))
|
||||
{
|
||||
@@ -378,34 +379,14 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
SType = StructureType.PhysicalDeviceFeatures2
|
||||
};
|
||||
|
||||
PhysicalDeviceVulkan11Features supportedFeaturesVk11 = new PhysicalDeviceVulkan11Features()
|
||||
PhysicalDeviceCustomBorderColorFeaturesEXT featuresCustomBorderColorSupported = new PhysicalDeviceCustomBorderColorFeaturesEXT()
|
||||
{
|
||||
SType = StructureType.PhysicalDeviceVulkan11Features,
|
||||
PNext = features2.PNext
|
||||
};
|
||||
|
||||
features2.PNext = &supportedFeaturesVk11;
|
||||
|
||||
PhysicalDeviceCustomBorderColorFeaturesEXT supportedFeaturesCustomBorderColor = new PhysicalDeviceCustomBorderColorFeaturesEXT()
|
||||
{
|
||||
SType = StructureType.PhysicalDeviceCustomBorderColorFeaturesExt,
|
||||
PNext = features2.PNext
|
||||
SType = StructureType.PhysicalDeviceCustomBorderColorFeaturesExt
|
||||
};
|
||||
|
||||
if (supportedExtensions.Contains("VK_EXT_custom_border_color"))
|
||||
{
|
||||
features2.PNext = &supportedFeaturesCustomBorderColor;
|
||||
}
|
||||
|
||||
PhysicalDeviceTransformFeedbackFeaturesEXT supportedFeaturesTransformFeedback = new PhysicalDeviceTransformFeedbackFeaturesEXT()
|
||||
{
|
||||
SType = StructureType.PhysicalDeviceTransformFeedbackFeaturesExt,
|
||||
PNext = features2.PNext
|
||||
};
|
||||
|
||||
if (supportedExtensions.Contains(ExtTransformFeedback.ExtensionName))
|
||||
{
|
||||
features2.PNext = &supportedFeaturesTransformFeedback;
|
||||
features2.PNext = &featuresCustomBorderColorSupported;
|
||||
}
|
||||
|
||||
PhysicalDeviceRobustness2FeaturesEXT supportedFeaturesRobustness2 = new PhysicalDeviceRobustness2FeaturesEXT()
|
||||
@@ -427,48 +408,41 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
var features = new PhysicalDeviceFeatures()
|
||||
{
|
||||
DepthBiasClamp = true,
|
||||
DepthClamp = supportedFeatures.DepthClamp,
|
||||
DualSrcBlend = supportedFeatures.DualSrcBlend,
|
||||
DepthClamp = true,
|
||||
DualSrcBlend = true,
|
||||
FragmentStoresAndAtomics = true,
|
||||
GeometryShader = supportedFeatures.GeometryShader,
|
||||
ImageCubeArray = true,
|
||||
IndependentBlend = true,
|
||||
LogicOp = supportedFeatures.LogicOp,
|
||||
MultiViewport = supportedFeatures.MultiViewport,
|
||||
MultiViewport = true,
|
||||
PipelineStatisticsQuery = supportedFeatures.PipelineStatisticsQuery,
|
||||
SamplerAnisotropy = true,
|
||||
ShaderClipDistance = true,
|
||||
ShaderFloat64 = supportedFeatures.ShaderFloat64,
|
||||
ShaderImageGatherExtended = supportedFeatures.ShaderImageGatherExtended,
|
||||
ShaderImageGatherExtended = true,
|
||||
ShaderStorageImageMultisample = supportedFeatures.ShaderStorageImageMultisample,
|
||||
// ShaderStorageImageReadWithoutFormat = true,
|
||||
// ShaderStorageImageWriteWithoutFormat = true,
|
||||
TessellationShader = supportedFeatures.TessellationShader,
|
||||
TessellationShader = true,
|
||||
VertexPipelineStoresAndAtomics = true,
|
||||
RobustBufferAccess = useRobustBufferAccess
|
||||
};
|
||||
|
||||
void* pExtendedFeatures = null;
|
||||
|
||||
PhysicalDeviceTransformFeedbackFeaturesEXT featuresTransformFeedback;
|
||||
|
||||
if (supportedExtensions.Contains(ExtTransformFeedback.ExtensionName))
|
||||
var featuresTransformFeedback = new PhysicalDeviceTransformFeedbackFeaturesEXT()
|
||||
{
|
||||
featuresTransformFeedback = new PhysicalDeviceTransformFeedbackFeaturesEXT()
|
||||
{
|
||||
SType = StructureType.PhysicalDeviceTransformFeedbackFeaturesExt,
|
||||
PNext = pExtendedFeatures,
|
||||
TransformFeedback = supportedFeaturesTransformFeedback.TransformFeedback
|
||||
};
|
||||
SType = StructureType.PhysicalDeviceTransformFeedbackFeaturesExt,
|
||||
PNext = pExtendedFeatures,
|
||||
TransformFeedback = true
|
||||
};
|
||||
|
||||
pExtendedFeatures = &featuresTransformFeedback;
|
||||
}
|
||||
|
||||
PhysicalDeviceRobustness2FeaturesEXT featuresRobustness2;
|
||||
pExtendedFeatures = &featuresTransformFeedback;
|
||||
|
||||
if (supportedExtensions.Contains("VK_EXT_robustness2"))
|
||||
{
|
||||
featuresRobustness2 = new PhysicalDeviceRobustness2FeaturesEXT()
|
||||
var featuresRobustness2 = new PhysicalDeviceRobustness2FeaturesEXT()
|
||||
{
|
||||
SType = StructureType.PhysicalDeviceRobustness2FeaturesExt,
|
||||
PNext = pExtendedFeatures,
|
||||
@@ -491,7 +465,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
{
|
||||
SType = StructureType.PhysicalDeviceVulkan11Features,
|
||||
PNext = pExtendedFeatures,
|
||||
ShaderDrawParameters = supportedFeaturesVk11.ShaderDrawParameters
|
||||
ShaderDrawParameters = true
|
||||
};
|
||||
|
||||
pExtendedFeatures = &featuresVk11;
|
||||
@@ -552,8 +526,8 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
PhysicalDeviceCustomBorderColorFeaturesEXT featuresCustomBorderColor;
|
||||
|
||||
if (supportedExtensions.Contains("VK_EXT_custom_border_color") &&
|
||||
supportedFeaturesCustomBorderColor.CustomBorderColors &&
|
||||
supportedFeaturesCustomBorderColor.CustomBorderColorWithoutFormat)
|
||||
featuresCustomBorderColorSupported.CustomBorderColors &&
|
||||
featuresCustomBorderColorSupported.CustomBorderColorWithoutFormat)
|
||||
{
|
||||
featuresCustomBorderColor = new PhysicalDeviceCustomBorderColorFeaturesEXT()
|
||||
{
|
||||
|
@@ -590,11 +590,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
|
||||
IsAmdWindows = Vendor == Vendor.Amd && RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
IsIntelWindows = Vendor == Vendor.Intel && RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
IsTBDR = IsMoltenVk ||
|
||||
Vendor == Vendor.Qualcomm ||
|
||||
Vendor == Vendor.ARM ||
|
||||
Vendor == Vendor.Broadcom ||
|
||||
Vendor == Vendor.ImgTec;
|
||||
IsTBDR = IsMoltenVk || Vendor == Vendor.Qualcomm || Vendor == Vendor.ARM || Vendor == Vendor.ImgTec;
|
||||
|
||||
GpuVendor = vendorName;
|
||||
GpuRenderer = Marshal.PtrToStringAnsi((IntPtr)properties.DeviceName);
|
||||
|
@@ -113,7 +113,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
ImageSharingMode = SharingMode.Exclusive,
|
||||
ImageArrayLayers = 1,
|
||||
PreTransform = capabilities.CurrentTransform,
|
||||
CompositeAlpha = ChooseCompositeAlpha(capabilities.SupportedCompositeAlpha),
|
||||
CompositeAlpha = CompositeAlphaFlagsKHR.OpaqueBitKhr,
|
||||
PresentMode = ChooseSwapPresentMode(presentModes, _vsyncEnabled),
|
||||
Clipped = true,
|
||||
OldSwapchain = oldSwapchain
|
||||
@@ -182,22 +182,6 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
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)
|
||||
{
|
||||
if (!vsyncEnabled && availablePresentModes.Contains(PresentModeKHR.ImmediateKhr))
|
||||
@@ -208,6 +192,10 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
{
|
||||
return PresentModeKHR.MailboxKhr;
|
||||
}
|
||||
else if (availablePresentModes.Contains(PresentModeKHR.FifoKhr))
|
||||
{
|
||||
return PresentModeKHR.FifoKhr;
|
||||
}
|
||||
else
|
||||
{
|
||||
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.Threading;
|
||||
using Ryujinx.Horizon.Common;
|
||||
|
@@ -10,6 +10,24 @@ namespace Ryujinx.HLE.HOS.Services.Pm
|
||||
{
|
||||
public IDebugMonitorInterface(ServiceCtx context) { }
|
||||
|
||||
[CommandHipc(4)]
|
||||
// GetProgramId() -> sf::Out<ncm::ProgramId> out_process_id
|
||||
public ResultCode GetApplicationProcessId(ServiceCtx context)
|
||||
{
|
||||
// TODO: Not correct as it shouldn't be directly using kernel objects here
|
||||
foreach (KProcess process in context.Device.System.KernelContext.Processes.Values)
|
||||
{
|
||||
if (process.IsApplication)
|
||||
{
|
||||
context.ResponseData.Write(process.Pid);
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
}
|
||||
|
||||
return ResultCode.ProcessNotFound;
|
||||
}
|
||||
|
||||
[CommandHipc(65000)]
|
||||
// AtmosphereGetProcessInfo(os::ProcessId process_id) -> sf::OutCopyHandle out_process_handle, sf::Out<ncm::ProgramLocation> out_loc, sf::Out<cfg::OverrideStatus> out_status
|
||||
public ResultCode GetProcessInfo(ServiceCtx context)
|
||||
|
@@ -1,8 +1,28 @@
|
||||
namespace Ryujinx.HLE.HOS.Services.Pm
|
||||
using Ryujinx.HLE.HOS.Kernel;
|
||||
using Ryujinx.HLE.HOS.Kernel.Process;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Pm
|
||||
{
|
||||
[Service("pm:info")]
|
||||
class IInformationInterface : IpcService
|
||||
{
|
||||
public IInformationInterface(ServiceCtx context) { }
|
||||
|
||||
[CommandHipc(0)]
|
||||
// GetProgramId(os::ProcessId process_id) -> sf::Out<ncm::ProgramId> out
|
||||
public ResultCode GetProgramId(ServiceCtx context)
|
||||
{
|
||||
ulong pid = context.RequestData.ReadUInt64();
|
||||
|
||||
// TODO: Not correct as it shouldn't be directly using kernel objects here
|
||||
if (context.Device.System.KernelContext.Processes.TryGetValue(pid, out KProcess process))
|
||||
{
|
||||
context.ResponseData.Write(process.TitleId);
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
return ResultCode.ProcessNotFound;
|
||||
}
|
||||
}
|
||||
}
|
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
|
||||
{
|
||||
private const int CursorHideIdleTime = 8; // seconds
|
||||
private const int CursorHideIdleTime = 5; // seconds
|
||||
|
||||
private bool _isDisposed;
|
||||
private HideCursor _hideCursor;
|
||||
|
@@ -435,12 +435,7 @@ namespace Ryujinx.Memory
|
||||
|
||||
public static ulong GetPageSize()
|
||||
{
|
||||
if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
||||
{
|
||||
return 1UL << 14;
|
||||
}
|
||||
|
||||
return 1UL << 12;
|
||||
return (ulong)Environment.SystemPageSize;
|
||||
}
|
||||
|
||||
private static void ThrowInvalidMemoryRegionException() => throw new InvalidMemoryRegionException();
|
||||
|
@@ -1,19 +1,75 @@
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.Ui.Common.Helper
|
||||
{
|
||||
public static class OpenHelper
|
||||
public static partial class OpenHelper
|
||||
{
|
||||
[LibraryImport("shell32.dll", SetLastError = true)]
|
||||
public static partial int SHOpenFolderAndSelectItems(IntPtr pidlFolder, uint cidl, IntPtr apidl, uint dwFlags);
|
||||
|
||||
[LibraryImport("shell32.dll", SetLastError = true)]
|
||||
public static partial void ILFree(IntPtr pidlList);
|
||||
|
||||
[LibraryImport("shell32.dll", SetLastError = true)]
|
||||
public static partial IntPtr ILCreateFromPathW([MarshalAs(UnmanagedType.LPWStr)] string pszPath);
|
||||
|
||||
public static void OpenFolder(string path)
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
FileName = path,
|
||||
UseShellExecute = true,
|
||||
Verb = "open"
|
||||
});
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = path,
|
||||
UseShellExecute = true,
|
||||
Verb = "open"
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Notice.Print(LogClass.Application, $"Directory \"{path}\" doesn't exist!");
|
||||
}
|
||||
}
|
||||
|
||||
public static void LocateFile(string path)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
IntPtr pidlList = ILCreateFromPathW(path);
|
||||
if (pidlList != IntPtr.Zero)
|
||||
{
|
||||
try
|
||||
{
|
||||
Marshal.ThrowExceptionForHR(SHOpenFolderAndSelectItems(pidlList, 0, IntPtr.Zero, 0));
|
||||
}
|
||||
finally
|
||||
{
|
||||
ILFree(pidlList);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
Process.Start("open", $"-R \"{path}\"");
|
||||
}
|
||||
else if (OperatingSystem.IsLinux())
|
||||
{
|
||||
Process.Start("dbus-send", $"--session --print-reply --dest=org.freedesktop.FileManager1 --type=method_call /org/freedesktop/FileManager1 org.freedesktop.FileManager1.ShowItems array:string:\"file://{path}\" string:\"\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
OpenFolder(Path.GetDirectoryName(path));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Notice.Print(LogClass.Application, $"File \"{path}\" doesn't exist!");
|
||||
}
|
||||
}
|
||||
|
||||
public static void OpenUrl(string url)
|
||||
|
@@ -68,7 +68,7 @@ namespace Ryujinx.Ui
|
||||
private readonly CancellationTokenSource _gpuCancellationTokenSource;
|
||||
|
||||
// Hide Cursor
|
||||
const int CursorHideIdleTime = 8; // seconds
|
||||
const int CursorHideIdleTime = 5; // seconds
|
||||
private static readonly Cursor _invisibleCursor = new Cursor(Display.Default, CursorType.BlankCursor);
|
||||
private long _lastCursorMoveTime;
|
||||
private bool _hideCursorOnIdle;
|
||||
|
Reference in New Issue
Block a user