Compare commits
44 Commits
Author | SHA1 | Date | |
---|---|---|---|
d692a9b83e | |||
9677ddaa5d | |||
ce92e8cd04 | |||
456fc04007 | |||
458452279c | |||
817b89767a | |||
3fb583c98c | |||
d2686e0a5b | |||
4905101df1 | |||
8750b90a7f | |||
af01100050 | |||
c0821fee1f | |||
a5c2aead67 | |||
d41c95dcff | |||
fbf2b09706 | |||
1fc0f569de | |||
dff138229c | |||
472119c8da | |||
1865ea87e5 | |||
18b61aff59 | |||
cb22629ac1 | |||
6f0f99ee2b | |||
70f2da8fdf | |||
5d3ef7761b | |||
476b4683cf | |||
5fb5079730 | |||
3fbacd0f49 | |||
7aa6abc120 | |||
548bfd60a2 | |||
65778a6b78 | |||
f4e879a1e6 | |||
a1ddaa2736 | |||
008286b79f | |||
a0c77f8d11 | |||
ece36b274d | |||
f3cc2e5703 | |||
5a39d3c4a1 | |||
cc51a03af9 | |||
567c64e149 | |||
36f00985d3 | |||
748d87adcc | |||
0fd47ff490 | |||
f088c3d344 | |||
905a191e28 |
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,6 +1,6 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Something doesn't work correctly in Ryujinx. Note that game-specific issues should be instead posted on the Game Compatibility List at https://github.com/Ryujinx/Ryujinx-Games-List, unless it is a provable regression.
|
||||
about: Something doesn't work correctly in Ryujinx. Game-specific issues should be posted at https://github.com/Ryujinx/Ryujinx-Games-List instead, unless it is a provable regression.
|
||||
#assignees:
|
||||
---
|
||||
|
||||
|
@ -36,7 +36,7 @@
|
||||
|
||||
## Compatibility
|
||||
|
||||
As of October 2022, Ryujinx has been tested on approximately 3,700 titles; over 3,500 boot past menus and into gameplay, with roughly 3,000 of those being considered playable.
|
||||
As of November 2022, Ryujinx has been tested on approximately 3,800 titles; over 3,600 boot past menus and into gameplay, with roughly 3,200 of those being considered playable.
|
||||
You can check out the compatibility list [here](https://github.com/Ryujinx/Ryujinx-Games-List/issues). Anyone is free to submit a new game test or update an existing game test entry; simply follow the new issue template and testing guidelines, or post as a reply to the applicable game issue. Use the search function to see if a game has been tested already!
|
||||
|
||||
## Usage
|
||||
@ -90,7 +90,7 @@ Ryujinx system files are stored in the `Ryujinx` folder. This folder is located
|
||||
|
||||
- **GPU**
|
||||
|
||||
The GPU emulator emulates the Switch's Maxwell GPU using either the OpenGL (version 4.5 minimum) or Vulkan APIs through a custom build of OpenTK or Silk.NET respectively. There are currently four graphics enhancements available to the end user in Ryujinx: Disk Shader Caching, Resolution Scaling, Aspect Ratio Adjustment, and Anisotropic Filtering. These enhancements can be adjusted or toggled as desired in the GUI.
|
||||
The GPU emulator emulates the Switch's Maxwell GPU using either the OpenGL (version 4.5 minimum), Vulkan, or Metal (via MoltenVK) APIs through a custom build of OpenTK or Silk.NET respectively. There are currently four graphics enhancements available to the end user in Ryujinx: Disk Shader Caching, Resolution Scaling, Aspect Ratio Adjustment, and Anisotropic Filtering. These enhancements can be adjusted or toggled as desired in the GUI.
|
||||
|
||||
- **Input**
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="OpenTK.OpenAL" Version="4.7.2" />
|
||||
<PackageReference Include="OpenTK.OpenAL" Version="4.7.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -1,5 +1,6 @@
|
||||
using Ryujinx.Audio.Common;
|
||||
using Ryujinx.Audio.Integration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Memory;
|
||||
using Ryujinx.SDL2.Common;
|
||||
using System;
|
||||
@ -112,6 +113,9 @@ namespace Ryujinx.Audio.Backends.SDL2
|
||||
|
||||
if (device == 0)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application,
|
||||
$"SDL2 open audio device initialization failed with error \"{SDL_GetError()}\"");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -119,6 +123,7 @@ namespace Ryujinx.Audio.Backends.SDL2
|
||||
|
||||
if (!isValid)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, "SDL2 open audio device is not valid");
|
||||
SDL_CloseAudioDevice(device);
|
||||
|
||||
return 0;
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Ryujinx.Audio.Renderer.Dsp.State;
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
@ -71,6 +72,19 @@ namespace Ryujinx.Audio.Renderer.Dsp
|
||||
return (short)value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static short GetCoefficientAtIndex(ReadOnlySpan<short> coefficients, int index)
|
||||
{
|
||||
if ((uint)index > (uint)coefficients.Length)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.AudioRenderer, $"Out of bound read for coefficient at index {index}");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
return coefficients[index];
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int Decode(Span<short> output, ReadOnlySpan<byte> input, int startSampleOffset, int endSampleOffset, int offset, int count, ReadOnlySpan<short> coefficients, ref AdpcmLoopContext loopContext)
|
||||
{
|
||||
@ -84,8 +98,8 @@ namespace Ryujinx.Audio.Renderer.Dsp
|
||||
byte coefficientIndex = (byte)((predScale >> 4) & 0xF);
|
||||
short history0 = loopContext.History0;
|
||||
short history1 = loopContext.History1;
|
||||
short coefficient0 = coefficients[coefficientIndex * 2 + 0];
|
||||
short coefficient1 = coefficients[coefficientIndex * 2 + 1];
|
||||
short coefficient0 = GetCoefficientAtIndex(coefficients, coefficientIndex * 2 + 0);
|
||||
short coefficient1 = GetCoefficientAtIndex(coefficients, coefficientIndex * 2 + 1);
|
||||
|
||||
int decodedCount = Math.Min(count, endSampleOffset - startSampleOffset - offset);
|
||||
int nibbles = GetNibblesFromSampleCount(offset + startSampleOffset);
|
||||
@ -109,8 +123,8 @@ namespace Ryujinx.Audio.Renderer.Dsp
|
||||
|
||||
coefficientIndex = (byte)((predScale >> 4) & 0xF);
|
||||
|
||||
coefficient0 = coefficients[coefficientIndex * 2 + 0];
|
||||
coefficient1 = coefficients[coefficientIndex * 2 + 1];
|
||||
coefficient0 = GetCoefficientAtIndex(coefficients, coefficientIndex * 2);
|
||||
coefficient1 = GetCoefficientAtIndex(coefficients, coefficientIndex * 2 + 1);
|
||||
|
||||
nibbles += 2;
|
||||
|
||||
|
@ -13,7 +13,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public CommandType CommandType => CommandType.AdpcmDataSourceVersion1;
|
||||
|
||||
public ulong EstimatedProcessingTime { get; set; }
|
||||
public uint EstimatedProcessingTime { get; set; }
|
||||
|
||||
public ushort OutputBufferIndex { get; }
|
||||
public uint SampleRate { get; }
|
||||
|
@ -16,7 +16,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public CommandType CommandType => CommandType.AuxiliaryBuffer;
|
||||
|
||||
public ulong EstimatedProcessingTime { get; set; }
|
||||
public uint EstimatedProcessingTime { get; set; }
|
||||
|
||||
public uint InputBufferIndex { get; }
|
||||
public uint OutputBufferIndex { get; }
|
||||
|
@ -12,7 +12,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public CommandType CommandType => CommandType.BiquadFilter;
|
||||
|
||||
public ulong EstimatedProcessingTime { get; set; }
|
||||
public uint EstimatedProcessingTime { get; set; }
|
||||
|
||||
public Memory<BiquadFilterState> BiquadFilterState { get; }
|
||||
public int InputBufferIndex { get; }
|
||||
|
@ -16,7 +16,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public CommandType CommandType => CommandType.CaptureBuffer;
|
||||
|
||||
public ulong EstimatedProcessingTime { get; set; }
|
||||
public uint EstimatedProcessingTime { get; set; }
|
||||
|
||||
public uint InputBufferIndex { get; }
|
||||
|
||||
|
@ -12,7 +12,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public CommandType CommandType => CommandType.CircularBufferSink;
|
||||
|
||||
public ulong EstimatedProcessingTime { get; set; }
|
||||
public uint EstimatedProcessingTime { get; set; }
|
||||
|
||||
public ushort[] Input { get; }
|
||||
public uint InputCount { get; }
|
||||
|
@ -8,7 +8,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public CommandType CommandType => CommandType.ClearMixBuffer;
|
||||
|
||||
public ulong EstimatedProcessingTime { get; set; }
|
||||
public uint EstimatedProcessingTime { get; set; }
|
||||
|
||||
public ClearMixBufferCommand(int nodeId)
|
||||
{
|
||||
|
@ -8,7 +8,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public CommandType CommandType => CommandType.CopyMixBuffer;
|
||||
|
||||
public ulong EstimatedProcessingTime { get; set; }
|
||||
public uint EstimatedProcessingTime { get; set; }
|
||||
|
||||
public ushort InputBufferIndex { get; }
|
||||
public ushort OutputBufferIndex { get; }
|
||||
|
@ -13,7 +13,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public CommandType CommandType { get; }
|
||||
|
||||
public ulong EstimatedProcessingTime { get; set; }
|
||||
public uint EstimatedProcessingTime { get; set; }
|
||||
|
||||
public ushort OutputBufferIndex { get; }
|
||||
public uint SampleRate { get; }
|
||||
|
@ -17,7 +17,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public CommandType CommandType => CommandType.Delay;
|
||||
|
||||
public ulong EstimatedProcessingTime { get; set; }
|
||||
public uint EstimatedProcessingTime { get; set; }
|
||||
|
||||
public DelayParameter Parameter => _parameter;
|
||||
public Memory<DelayState> State { get; }
|
||||
@ -49,15 +49,15 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
OutputBufferIndices[i] = (ushort)(bufferOffset + Parameter.Output[i]);
|
||||
}
|
||||
|
||||
// NOTE: We do the opposite as Nintendo here for now to restore previous behaviour
|
||||
// TODO: Update delay processing and remove this to use RemapLegacyChannelEffectMappingToChannelResourceMapping.
|
||||
DataSourceHelper.RemapChannelResourceMappingToLegacy(newEffectChannelMappingSupported, InputBufferIndices);
|
||||
DataSourceHelper.RemapChannelResourceMappingToLegacy(newEffectChannelMappingSupported, OutputBufferIndices);
|
||||
DataSourceHelper.RemapLegacyChannelEffectMappingToChannelResourceMapping(newEffectChannelMappingSupported, InputBufferIndices);
|
||||
DataSourceHelper.RemapLegacyChannelEffectMappingToChannelResourceMapping(newEffectChannelMappingSupported, OutputBufferIndices);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private unsafe void ProcessDelayMono(ref DelayState state, float* outputBuffer, float* inputBuffer, uint sampleCount)
|
||||
{
|
||||
const ushort channelCount = 1;
|
||||
|
||||
float feedbackGain = FixedPointHelper.ToFloat(Parameter.FeedbackGain, FixedPointPrecision);
|
||||
float inGain = FixedPointHelper.ToFloat(Parameter.InGain, FixedPointPrecision);
|
||||
float dryGain = FixedPointHelper.ToFloat(Parameter.DryGain, FixedPointPrecision);
|
||||
@ -70,7 +70,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
float temp = input * inGain + delayLineValue * feedbackGain;
|
||||
|
||||
state.UpdateLowPassFilter(ref temp, 1);
|
||||
state.UpdateLowPassFilter(ref temp, channelCount);
|
||||
|
||||
outputBuffer[i] = (input * dryGain + delayLineValue * outGain) / 64;
|
||||
}
|
||||
@ -104,7 +104,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
Y = state.DelayLines[1].Read(),
|
||||
};
|
||||
|
||||
Vector2 temp = MatrixHelper.Transform(ref channelInput, ref delayFeedback) + channelInput * inGain;
|
||||
Vector2 temp = MatrixHelper.Transform(ref delayLineValues, ref delayFeedback) + channelInput * inGain;
|
||||
|
||||
state.UpdateLowPassFilter(ref Unsafe.As<Vector2, float>(ref temp), channelCount);
|
||||
|
||||
@ -148,7 +148,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
W = state.DelayLines[3].Read()
|
||||
};
|
||||
|
||||
Vector4 temp = MatrixHelper.Transform(ref channelInput, ref delayFeedback) + channelInput * inGain;
|
||||
Vector4 temp = MatrixHelper.Transform(ref delayLineValues, ref delayFeedback) + channelInput * inGain;
|
||||
|
||||
state.UpdateLowPassFilter(ref Unsafe.As<Vector4, float>(ref temp), channelCount);
|
||||
|
||||
@ -171,12 +171,12 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
float dryGain = FixedPointHelper.ToFloat(Parameter.DryGain, FixedPointPrecision);
|
||||
float outGain = FixedPointHelper.ToFloat(Parameter.OutGain, FixedPointPrecision);
|
||||
|
||||
Matrix6x6 delayFeedback = new Matrix6x6(delayFeedbackBaseGain, 0.0f, 0.0f, 0.0f, delayFeedbackCrossGain, delayFeedbackCrossGain,
|
||||
0.0f, delayFeedbackBaseGain, 0.0f, delayFeedbackCrossGain, delayFeedbackCrossGain, 0.0f,
|
||||
delayFeedbackCrossGain, 0.0f, delayFeedbackBaseGain, delayFeedbackCrossGain, 0.0f, 0.0f,
|
||||
0.0f, delayFeedbackCrossGain, delayFeedbackCrossGain, delayFeedbackBaseGain, 0.0f, 0.0f,
|
||||
delayFeedbackCrossGain, delayFeedbackCrossGain, 0.0f, 0.0f, delayFeedbackBaseGain, 0.0f,
|
||||
0.0f, 0.0f, 0.0f, 0.0f, 0.0f, feedbackGain);
|
||||
Matrix6x6 delayFeedback = new Matrix6x6(delayFeedbackBaseGain, 0.0f, delayFeedbackCrossGain, 0.0f, delayFeedbackCrossGain, 0.0f,
|
||||
0.0f, delayFeedbackBaseGain, delayFeedbackCrossGain, 0.0f, 0.0f, delayFeedbackCrossGain,
|
||||
delayFeedbackCrossGain, delayFeedbackCrossGain, delayFeedbackBaseGain, 0.0f, 0.0f, 0.0f,
|
||||
0.0f, 0.0f, 0.0f, feedbackGain, 0.0f, 0.0f,
|
||||
delayFeedbackCrossGain, 0.0f, 0.0f, 0.0f, delayFeedbackBaseGain, delayFeedbackCrossGain,
|
||||
0.0f, delayFeedbackCrossGain, 0.0f, 0.0f, delayFeedbackCrossGain, delayFeedbackBaseGain);
|
||||
|
||||
for (int i = 0; i < sampleCount; i++)
|
||||
{
|
||||
@ -200,7 +200,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
U = state.DelayLines[5].Read()
|
||||
};
|
||||
|
||||
Vector6 temp = MatrixHelper.Transform(ref channelInput, ref delayFeedback) + channelInput * inGain;
|
||||
Vector6 temp = MatrixHelper.Transform(ref delayLineValues, ref delayFeedback) + channelInput * inGain;
|
||||
|
||||
state.UpdateLowPassFilter(ref Unsafe.As<Vector6, float>(ref temp), channelCount);
|
||||
|
||||
|
@ -11,7 +11,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public CommandType CommandType => CommandType.DepopForMixBuffers;
|
||||
|
||||
public ulong EstimatedProcessingTime { get; set; }
|
||||
public uint EstimatedProcessingTime { get; set; }
|
||||
|
||||
public uint MixBufferOffset { get; }
|
||||
|
||||
|
@ -11,7 +11,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public CommandType CommandType => CommandType.DepopPrepare;
|
||||
|
||||
public ulong EstimatedProcessingTime { get; set; }
|
||||
public uint EstimatedProcessingTime { get; set; }
|
||||
|
||||
public uint MixBufferCount { get; }
|
||||
|
||||
|
@ -14,7 +14,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public CommandType CommandType => CommandType.DeviceSink;
|
||||
|
||||
public ulong EstimatedProcessingTime { get; set; }
|
||||
public uint EstimatedProcessingTime { get; set; }
|
||||
|
||||
public string DeviceName { get; }
|
||||
|
||||
|
@ -11,7 +11,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public CommandType CommandType => CommandType.DownMixSurroundToStereo;
|
||||
|
||||
public ulong EstimatedProcessingTime { get; set; }
|
||||
public uint EstimatedProcessingTime { get; set; }
|
||||
|
||||
public ushort[] InputBufferIndices { get; }
|
||||
public ushort[] OutputBufferIndices { get; }
|
||||
|
@ -12,7 +12,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public CommandType CommandType => CommandType.GroupedBiquadFilter;
|
||||
|
||||
public ulong EstimatedProcessingTime { get; set; }
|
||||
public uint EstimatedProcessingTime { get; set; }
|
||||
|
||||
private BiquadFilterParameter[] _parameters;
|
||||
private Memory<BiquadFilterState> _biquadFilterStates;
|
||||
@ -47,9 +47,8 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Nintendo also implements a hot path for double biquad filters, but no generic path when the command definition suggests it could be done.
|
||||
// As such we currently only implement a generic path for simplicity.
|
||||
// TODO: Implement double biquad filters fast path.
|
||||
// NOTE: Nintendo only implement single and double biquad filters but no generic path when the command definition suggests it could be done.
|
||||
// As such we currently only implement a generic path for simplicity for double biquad.
|
||||
if (_parameters.Length == 1)
|
||||
{
|
||||
BiquadFilterHelper.ProcessBiquadFilter(ref _parameters[0], ref states[0], outputBuffer, inputBuffer, context.SampleCount);
|
||||
|
@ -8,7 +8,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public CommandType CommandType { get; }
|
||||
|
||||
public ulong EstimatedProcessingTime { get; }
|
||||
public uint EstimatedProcessingTime { get; }
|
||||
|
||||
public void Process(CommandList context);
|
||||
|
||||
|
@ -13,7 +13,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public CommandType CommandType => CommandType.LimiterVersion1;
|
||||
|
||||
public ulong EstimatedProcessingTime { get; set; }
|
||||
public uint EstimatedProcessingTime { get; set; }
|
||||
|
||||
public LimiterParameter Parameter => _parameter;
|
||||
public Memory<LimiterState> State { get; }
|
||||
|
@ -15,7 +15,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public CommandType CommandType => CommandType.LimiterVersion2;
|
||||
|
||||
public ulong EstimatedProcessingTime { get; set; }
|
||||
public uint EstimatedProcessingTime { get; set; }
|
||||
|
||||
public LimiterParameter Parameter => _parameter;
|
||||
public Memory<LimiterState> State { get; }
|
||||
|
@ -15,7 +15,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public CommandType CommandType => CommandType.Mix;
|
||||
|
||||
public ulong EstimatedProcessingTime { get; set; }
|
||||
public uint EstimatedProcessingTime { get; set; }
|
||||
|
||||
public ushort InputBufferIndex { get; }
|
||||
public ushort OutputBufferIndex { get; }
|
||||
|
@ -12,7 +12,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public CommandType CommandType => CommandType.MixRamp;
|
||||
|
||||
public ulong EstimatedProcessingTime { get; set; }
|
||||
public uint EstimatedProcessingTime { get; set; }
|
||||
|
||||
public ushort InputBufferIndex { get; }
|
||||
public ushort OutputBufferIndex { get; }
|
||||
|
@ -12,7 +12,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public CommandType CommandType => CommandType.MixRampGrouped;
|
||||
|
||||
public ulong EstimatedProcessingTime { get; set; }
|
||||
public uint EstimatedProcessingTime { get; set; }
|
||||
|
||||
public uint MixBufferCount { get; }
|
||||
|
||||
|
@ -13,7 +13,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public CommandType CommandType => CommandType.PcmFloatDataSourceVersion1;
|
||||
|
||||
public ulong EstimatedProcessingTime { get; set; }
|
||||
public uint EstimatedProcessingTime { get; set; }
|
||||
|
||||
public ushort OutputBufferIndex { get; }
|
||||
public uint SampleRate { get; }
|
||||
|
@ -13,7 +13,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public CommandType CommandType => CommandType.PcmInt16DataSourceVersion1;
|
||||
|
||||
public ulong EstimatedProcessingTime { get; set; }
|
||||
public uint EstimatedProcessingTime { get; set; }
|
||||
|
||||
public ushort OutputBufferIndex { get; }
|
||||
public uint SampleRate { get; }
|
||||
|
@ -17,7 +17,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public CommandType CommandType => CommandType.Performance;
|
||||
|
||||
public ulong EstimatedProcessingTime { get; set; }
|
||||
public uint EstimatedProcessingTime { get; set; }
|
||||
|
||||
public PerformanceEntryAddresses PerformanceEntryAddresses { get; }
|
||||
|
||||
|
@ -31,7 +31,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public CommandType CommandType => CommandType.Reverb3d;
|
||||
|
||||
public ulong EstimatedProcessingTime { get; set; }
|
||||
public uint EstimatedProcessingTime { get; set; }
|
||||
|
||||
public ushort InputBufferIndex { get; }
|
||||
public ushort OutputBufferIndex { get; }
|
||||
|
@ -34,7 +34,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public CommandType CommandType => CommandType.Reverb;
|
||||
|
||||
public ulong EstimatedProcessingTime { get; set; }
|
||||
public uint EstimatedProcessingTime { get; set; }
|
||||
|
||||
public ReverbParameter Parameter => _parameter;
|
||||
public Memory<ReverbState> State { get; }
|
||||
|
@ -11,7 +11,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public CommandType CommandType => CommandType.Upsample;
|
||||
|
||||
public ulong EstimatedProcessingTime { get; set; }
|
||||
public uint EstimatedProcessingTime { get; set; }
|
||||
|
||||
public uint BufferCount { get; }
|
||||
public uint InputBufferIndex { get; }
|
||||
|
@ -15,7 +15,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public CommandType CommandType => CommandType.Volume;
|
||||
|
||||
public ulong EstimatedProcessingTime { get; set; }
|
||||
public uint EstimatedProcessingTime { get; set; }
|
||||
|
||||
public ushort InputBufferIndex { get; }
|
||||
public ushort OutputBufferIndex { get; }
|
||||
|
@ -11,7 +11,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public CommandType CommandType => CommandType.VolumeRamp;
|
||||
|
||||
public ulong EstimatedProcessingTime { get; set; }
|
||||
public uint EstimatedProcessingTime { get; set; }
|
||||
|
||||
public ushort InputBufferIndex { get; }
|
||||
public ushort OutputBufferIndex { get; }
|
||||
|
@ -28,6 +28,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
{
|
||||
private object _lock = new object();
|
||||
|
||||
private AudioRendererRenderingDevice _renderingDevice;
|
||||
private AudioRendererExecutionMode _executionMode;
|
||||
private IWritableEvent _systemEvent;
|
||||
private ManualResetEvent _terminationEvent;
|
||||
@ -63,6 +64,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
private uint _renderingTimeLimitPercent;
|
||||
private bool _voiceDropEnabled;
|
||||
private uint _voiceDropCount;
|
||||
private float _voiceDropParameter;
|
||||
private bool _isDspRunningBehind;
|
||||
|
||||
private ICommandProcessingTimeEstimator _commandProcessingTimeEstimator;
|
||||
@ -95,6 +97,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
|
||||
_totalElapsedTicksUpdating = 0;
|
||||
_sessionId = 0;
|
||||
_voiceDropParameter = 1.0f;
|
||||
}
|
||||
|
||||
public ResultCode Initialize(
|
||||
@ -130,6 +133,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
_upsamplerCount = parameter.SinkCount + parameter.SubMixBufferCount;
|
||||
_appletResourceId = appletResourceId;
|
||||
_memoryPoolCount = parameter.EffectCount + parameter.VoiceCount * Constants.VoiceWaveBufferCount;
|
||||
_renderingDevice = parameter.RenderingDevice;
|
||||
_executionMode = parameter.ExecutionMode;
|
||||
_sessionId = sessionId;
|
||||
MemoryManager = memoryManager;
|
||||
@ -337,6 +341,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
|
||||
_processHandle = processHandle;
|
||||
_elapsedFrameCount = 0;
|
||||
_voiceDropParameter = 1.0f;
|
||||
|
||||
switch (_behaviourContext.GetCommandProcessingTimeEstimatorVersion())
|
||||
{
|
||||
@ -515,7 +520,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
return (ulong)(_manager.TickSource.ElapsedSeconds * Constants.TargetTimerFrequency);
|
||||
}
|
||||
|
||||
private uint ComputeVoiceDrop(CommandBuffer commandBuffer, long voicesEstimatedTime, long deltaTimeDsp)
|
||||
private uint ComputeVoiceDrop(CommandBuffer commandBuffer, uint voicesEstimatedTime, long deltaTimeDsp)
|
||||
{
|
||||
int i;
|
||||
|
||||
@ -584,7 +589,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
{
|
||||
command.Enabled = false;
|
||||
|
||||
voicesEstimatedTime -= (long)command.EstimatedProcessingTime;
|
||||
voicesEstimatedTime -= (uint)(_voiceDropParameter * command.EstimatedProcessingTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -618,13 +623,13 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
_voiceContext.Sort();
|
||||
commandGenerator.GenerateVoices();
|
||||
|
||||
long voicesEstimatedTime = (long)commandBuffer.EstimatedProcessingTime;
|
||||
uint voicesEstimatedTime = (uint)(_voiceDropParameter * commandBuffer.EstimatedProcessingTime);
|
||||
|
||||
commandGenerator.GenerateSubMixes();
|
||||
commandGenerator.GenerateFinalMixes();
|
||||
commandGenerator.GenerateSinks();
|
||||
|
||||
long totalEstimatedTime = (long)commandBuffer.EstimatedProcessingTime;
|
||||
uint totalEstimatedTime = (uint)(_voiceDropParameter * commandBuffer.EstimatedProcessingTime);
|
||||
|
||||
if (_voiceDropEnabled)
|
||||
{
|
||||
@ -856,5 +861,26 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetVoiceDropParameter(float voiceDropParameter)
|
||||
{
|
||||
_voiceDropParameter = Math.Clamp(voiceDropParameter, 0.0f, 2.0f);
|
||||
}
|
||||
|
||||
public float GetVoiceDropParameter()
|
||||
{
|
||||
return _voiceDropParameter;
|
||||
}
|
||||
|
||||
public ResultCode ExecuteAudioRendererRendering()
|
||||
{
|
||||
if (_executionMode == AudioRendererExecutionMode.Manual && _renderingDevice == AudioRendererRenderingDevice.Cpu)
|
||||
{
|
||||
// NOTE: Here Nintendo aborts with this error code, we don't want that.
|
||||
return ResultCode.InvalidExecutionContextOperation;
|
||||
}
|
||||
|
||||
return ResultCode.UnsupportedOperation;
|
||||
}
|
||||
}
|
||||
}
|
@ -94,8 +94,9 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
/// REV11:
|
||||
/// The "legacy" effects (Delay, Reverb and Reverb 3D) were updated to match the standard channel mapping used by the audio renderer.
|
||||
/// A new version of the command estimator was added to address timing changes caused by the legacy effects changes.
|
||||
/// A voice drop parameter was added in 15.0.0: This allows an application to amplify or attenuate the estimated time of DSP commands.
|
||||
/// </summary>
|
||||
/// <remarks>This was added in system update 14.0.0</remarks>
|
||||
/// <remarks>This was added in system update 14.0.0 but some changes were made in 15.0.0</remarks>
|
||||
public const int Revision11 = 11 << 24;
|
||||
|
||||
/// <summary>
|
||||
|
@ -25,7 +25,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
/// <summary>
|
||||
/// The estimated total processing time.
|
||||
/// </summary>
|
||||
public ulong EstimatedProcessingTime { get; set; }
|
||||
public uint EstimatedProcessingTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The command list that is populated by the <see cref="CommandBuffer"/>.
|
||||
|
@ -263,12 +263,12 @@ namespace Ryujinx.Audio.Renderer.Server.MemoryPool
|
||||
return UpdateResult.Success;
|
||||
}
|
||||
|
||||
if (inParameter.CpuAddress == 0 || (inParameter.CpuAddress & (pageSize - 1)) != 0)
|
||||
if (inParameter.CpuAddress == 0 || (inParameter.CpuAddress % pageSize) != 0)
|
||||
{
|
||||
return UpdateResult.InvalidParameter;
|
||||
}
|
||||
|
||||
if (inParameter.Size == 0 || (inParameter.Size & (pageSize - 1)) != 0)
|
||||
if (inParameter.Size == 0 || (inParameter.Size % pageSize) != 0)
|
||||
{
|
||||
return UpdateResult.InvalidParameter;
|
||||
}
|
||||
|
@ -17,5 +17,6 @@ namespace Ryujinx.Audio
|
||||
InvalidAddressInfo = (42 << ErrorCodeShift) | ModuleId,
|
||||
InvalidMixSorting = (43 << ErrorCodeShift) | ModuleId,
|
||||
UnsupportedOperation = (513 << ErrorCodeShift) | ModuleId,
|
||||
InvalidExecutionContextOperation = (514 << ErrorCodeShift) | ModuleId,
|
||||
}
|
||||
}
|
@ -21,6 +21,8 @@ namespace Ryujinx.Ava
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
Name = $"Ryujinx {Program.Version}";
|
||||
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
|
@ -60,7 +60,7 @@ namespace Ryujinx.Ava
|
||||
|
||||
private const float VolumeDelta = 0.05f;
|
||||
|
||||
private static readonly Cursor InvisibleCursor = new Cursor(StandardCursorType.None);
|
||||
private static readonly Cursor InvisibleCursor = new Cursor(StandardCursorType.None);
|
||||
|
||||
private readonly long _ticksPerFrame;
|
||||
private readonly Stopwatch _chrono;
|
||||
@ -349,7 +349,10 @@ namespace Ryujinx.Ava
|
||||
|
||||
_isActive = false;
|
||||
|
||||
_renderingThread.Join();
|
||||
if (_renderingThread.IsAlive)
|
||||
{
|
||||
_renderingThread.Join();
|
||||
}
|
||||
|
||||
DisplaySleep.Restore();
|
||||
|
||||
@ -378,7 +381,7 @@ namespace Ryujinx.Ava
|
||||
|
||||
_gpuCancellationTokenSource.Cancel();
|
||||
_gpuCancellationTokenSource.Dispose();
|
||||
|
||||
|
||||
_chrono.Stop();
|
||||
}
|
||||
|
||||
@ -393,7 +396,7 @@ namespace Ryujinx.Ava
|
||||
Renderer?.MakeCurrent();
|
||||
|
||||
Device.DisposeGpu();
|
||||
|
||||
|
||||
Renderer?.MakeCurrent(null);
|
||||
}
|
||||
|
||||
@ -417,7 +420,6 @@ namespace Ryujinx.Ava
|
||||
public async Task<bool> LoadGuestApplication()
|
||||
{
|
||||
InitializeSwitchInstance();
|
||||
|
||||
MainWindow.UpdateGraphicsConfig();
|
||||
|
||||
SystemVersion firmwareVersion = ContentManager.GetCurrentFirmwareVersion();
|
||||
@ -428,17 +430,16 @@ namespace Ryujinx.Ava
|
||||
{
|
||||
if (userError == UserError.NoFirmware)
|
||||
{
|
||||
string message = string.Format(LocaleManager.Instance["DialogFirmwareInstallEmbeddedMessage"],
|
||||
firmwareVersion.VersionString);
|
||||
|
||||
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
|
||||
LocaleManager.Instance["DialogFirmwareNoFirmwareInstalledMessage"], message,
|
||||
LocaleManager.Instance["InputDialogYes"], LocaleManager.Instance["InputDialogNo"], "");
|
||||
LocaleManager.Instance["DialogFirmwareNoFirmwareInstalledMessage"],
|
||||
string.Format(LocaleManager.Instance["DialogFirmwareInstallEmbeddedMessage"], firmwareVersion.VersionString),
|
||||
LocaleManager.Instance["InputDialogYes"],
|
||||
LocaleManager.Instance["InputDialogNo"],
|
||||
"");
|
||||
|
||||
if (result != UserResult.Yes)
|
||||
{
|
||||
Dispatcher.UIThread.Post(async () => await
|
||||
UserErrorDialog.ShowUserErrorDialog(userError, _parent));
|
||||
await UserErrorDialog.ShowUserErrorDialog(userError, _parent);
|
||||
Device.Dispose();
|
||||
|
||||
return false;
|
||||
@ -447,8 +448,7 @@ namespace Ryujinx.Ava
|
||||
|
||||
if (!SetupValidator.TryFixStartApplication(ContentManager, ApplicationPath, userError, out _))
|
||||
{
|
||||
Dispatcher.UIThread.Post(async () => await
|
||||
UserErrorDialog.ShowUserErrorDialog(userError, _parent));
|
||||
await UserErrorDialog.ShowUserErrorDialog(userError, _parent);
|
||||
Device.Dispose();
|
||||
|
||||
return false;
|
||||
@ -461,11 +461,9 @@ namespace Ryujinx.Ava
|
||||
|
||||
_parent.RefreshFirmwareStatus();
|
||||
|
||||
string message = string.Format(LocaleManager.Instance["DialogFirmwareInstallEmbeddedSuccessMessage"], firmwareVersion.VersionString);
|
||||
|
||||
await ContentDialogHelper.CreateInfoDialog(
|
||||
string.Format(LocaleManager.Instance["DialogFirmwareInstalledMessage"], firmwareVersion.VersionString),
|
||||
message,
|
||||
string.Format(LocaleManager.Instance["DialogFirmwareInstallEmbeddedSuccessMessage"], firmwareVersion.VersionString),
|
||||
LocaleManager.Instance["InputDialogOk"],
|
||||
"",
|
||||
LocaleManager.Instance["RyujinxInfo"]);
|
||||
@ -473,9 +471,7 @@ namespace Ryujinx.Ava
|
||||
}
|
||||
else
|
||||
{
|
||||
Dispatcher.UIThread.Post(async () => await
|
||||
UserErrorDialog.ShowUserErrorDialog(userError, _parent));
|
||||
|
||||
await UserErrorDialog.ShowUserErrorDialog(userError, _parent);
|
||||
Device.Dispose();
|
||||
|
||||
return false;
|
||||
@ -514,7 +510,7 @@ namespace Ryujinx.Ava
|
||||
}
|
||||
else if (File.Exists(ApplicationPath))
|
||||
{
|
||||
switch (System.IO.Path.GetExtension(ApplicationPath).ToLowerInvariant())
|
||||
switch (Path.GetExtension(ApplicationPath).ToLowerInvariant())
|
||||
{
|
||||
case ".xci":
|
||||
{
|
||||
@ -602,7 +598,7 @@ namespace Ryujinx.Ava
|
||||
if (Renderer.IsVulkan)
|
||||
{
|
||||
string preferredGpu = ConfigurationState.Instance.Graphics.PreferredGpu.Value;
|
||||
|
||||
|
||||
renderer = new VulkanRenderer(Renderer.CreateVulkanSurface, VulkanHelper.GetRequiredInstanceExtensions, preferredGpu);
|
||||
}
|
||||
else
|
||||
|
@ -410,6 +410,8 @@
|
||||
"DlcManagerTableHeadingContainerPathLabel": "Container Path",
|
||||
"DlcManagerTableHeadingFullPathLabel": "Full Path",
|
||||
"DlcManagerRemoveAllButton": "Remove All",
|
||||
"DlcManagerEnableAllButton": "Enable All",
|
||||
"DlcManagerDisableAllButton": "Disable All",
|
||||
"MenuBarOptionsChangeLanguage": "Change Language",
|
||||
"CommonSort": "Sort",
|
||||
"CommonShowNames": "Show Names",
|
||||
@ -562,12 +564,12 @@
|
||||
"Writable": "Writable",
|
||||
"SelectDlcDialogTitle": "Select DLC files",
|
||||
"SelectUpdateDialogTitle": "Select update files",
|
||||
"UserProfileWindowTitle": "Manage User Profiles",
|
||||
"CheatWindowTitle": "Manage Game Cheats",
|
||||
"DlcWindowTitle": "Manage Game DLC",
|
||||
"UpdateWindowTitle": "Manage Game Updates",
|
||||
"UserProfileWindowTitle": "User Profiles Manager",
|
||||
"CheatWindowTitle": "Cheats Manager",
|
||||
"DlcWindowTitle": "Downloadable Content Manager",
|
||||
"UpdateWindowTitle": "Title Update Manager",
|
||||
"CheatWindowHeading": "Cheats Available for {0} [{1}]",
|
||||
"DlcWindowHeading": "DLC Available for {0} [{1}]",
|
||||
"DlcWindowHeading": "{0} Downloadable Content(s) available for {1} ({2})",
|
||||
"UserProfilesEditProfile": "Edit Selected",
|
||||
"Cancel": "Cancel",
|
||||
"Save": "Save",
|
||||
@ -575,7 +577,7 @@
|
||||
"UserProfilesSetProfileImage": "Set Profile Image",
|
||||
"UserProfileEmptyNameError": "Name is required",
|
||||
"UserProfileNoImageError": "Profile image must be set",
|
||||
"GameUpdateWindowHeading": "Updates Available for {0} [{1}]",
|
||||
"GameUpdateWindowHeading": "{0} Update(s) available for {1} ({2})",
|
||||
"SettingsTabHotkeysResScaleUpHotkey": "Increase resolution:",
|
||||
"SettingsTabHotkeysResScaleDownHotkey": "Decrease resolution:",
|
||||
"UserProfilesName": "Name:",
|
||||
|
@ -1,5 +1,6 @@
|
||||
using Ryujinx.Ava.Ui.ViewModels;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.Ui.Common.Configuration;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
@ -93,7 +94,7 @@ namespace Ryujinx.Ava.Common.Locale
|
||||
return;
|
||||
}
|
||||
|
||||
var strings = JsonSerializer.Deserialize<Dictionary<string, string>>(languageJson);
|
||||
var strings = JsonHelper.Deserialize<Dictionary<string, string>>(languageJson);
|
||||
|
||||
foreach (var item in strings)
|
||||
{
|
||||
|
@ -1,8 +1,5 @@
|
||||
using ARMeilleure.Translation.PTC;
|
||||
using Avalonia;
|
||||
using Avalonia.Rendering;
|
||||
using Avalonia.Threading;
|
||||
using Ryujinx.Ava.Ui.Controls;
|
||||
using Ryujinx.Ava.Ui.Windows;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration;
|
||||
@ -24,18 +21,14 @@ namespace Ryujinx.Ava
|
||||
internal class Program
|
||||
{
|
||||
public static double WindowScaleFactor { get; set; }
|
||||
public static double ActualScaleFactor { get; set; }
|
||||
public static string Version { get; private set; }
|
||||
public static string Version { get; private set; }
|
||||
public static string ConfigurationPath { get; private set; }
|
||||
public static bool PreviewerDetached { get; private set; }
|
||||
|
||||
public static RenderTimer RenderTimer { get; private set; }
|
||||
public static bool PreviewerDetached { get; private set; }
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern int MessageBoxA(IntPtr hWnd, string text, string caption, uint type);
|
||||
|
||||
private const uint MB_ICONWARNING = 0x30;
|
||||
private const int BaseDpi = 96;
|
||||
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
@ -43,18 +36,14 @@ namespace Ryujinx.Ava
|
||||
|
||||
if (OperatingSystem.IsWindows() && !OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17134))
|
||||
{
|
||||
MessageBoxA(IntPtr.Zero, "You are running an outdated version of Windows.\n\nStarting on June 1st 2022, Ryujinx will only support Windows 10 1803 and newer.\n", $"Ryujinx {Version}", MB_ICONWARNING);
|
||||
_ = MessageBoxA(IntPtr.Zero, "You are running an outdated version of Windows.\n\nStarting on June 1st 2022, Ryujinx will only support Windows 10 1803 and newer.\n", $"Ryujinx {Version}", MB_ICONWARNING);
|
||||
}
|
||||
|
||||
PreviewerDetached = true;
|
||||
|
||||
Initialize(args);
|
||||
|
||||
RenderTimer = new RenderTimer();
|
||||
|
||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
||||
|
||||
RenderTimer.Dispose();
|
||||
}
|
||||
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
@ -64,24 +53,18 @@ namespace Ryujinx.Ava
|
||||
.With(new X11PlatformOptions
|
||||
{
|
||||
EnableMultiTouch = true,
|
||||
EnableIme = true,
|
||||
UseEGL = false,
|
||||
UseGpu = false
|
||||
EnableIme = true,
|
||||
UseEGL = false,
|
||||
UseGpu = true
|
||||
})
|
||||
.With(new Win32PlatformOptions
|
||||
{
|
||||
EnableMultitouch = true,
|
||||
UseWgl = false,
|
||||
AllowEglInitialization = false,
|
||||
CompositionBackdropCornerRadius = 8f,
|
||||
EnableMultitouch = true,
|
||||
UseWgl = false,
|
||||
AllowEglInitialization = false,
|
||||
CompositionBackdropCornerRadius = 8.0f,
|
||||
})
|
||||
.UseSkia()
|
||||
.AfterSetup(_ =>
|
||||
{
|
||||
AvaloniaLocator.CurrentMutable
|
||||
.Bind<IRenderTimer>().ToConstant(RenderTimer)
|
||||
.Bind<IRenderLoop>().ToConstant(new RenderLoop(RenderTimer, Dispatcher.UIThread));
|
||||
})
|
||||
.LogToTrace();
|
||||
}
|
||||
|
||||
@ -116,18 +99,15 @@ namespace Ryujinx.Ava
|
||||
ForceDpiAware.Windows();
|
||||
|
||||
WindowScaleFactor = ForceDpiAware.GetWindowScaleFactor();
|
||||
ActualScaleFactor = ForceDpiAware.GetActualScaleFactor() / BaseDpi;
|
||||
|
||||
// Logging system information.
|
||||
PrintSystemInfo();
|
||||
|
||||
// Enable OGL multithreading on the driver, when available.
|
||||
BackendThreading threadingMode = ConfigurationState.Instance.Graphics.BackendThreading;
|
||||
DriverUtilities.ToggleOGLThreading(threadingMode == BackendThreading.Off);
|
||||
DriverUtilities.ToggleOGLThreading(ConfigurationState.Instance.Graphics.BackendThreading == BackendThreading.Off);
|
||||
|
||||
// Check if keys exists.
|
||||
bool hasSystemProdKeys = File.Exists(Path.Combine(AppDataManager.KeysDirPath, "prod.keys"));
|
||||
if (!hasSystemProdKeys)
|
||||
if (!File.Exists(Path.Combine(AppDataManager.KeysDirPath, "prod.keys")))
|
||||
{
|
||||
if (!(AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile && File.Exists(Path.Combine(AppDataManager.KeysDirPathUser, "prod.keys"))))
|
||||
{
|
||||
@ -143,7 +123,7 @@ namespace Ryujinx.Ava
|
||||
|
||||
public static void ReloadConfig()
|
||||
{
|
||||
string localConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config.json");
|
||||
string localConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config.json");
|
||||
string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, "Config.json");
|
||||
|
||||
// Now load the configuration as the other subsystems are now registered
|
||||
@ -197,8 +177,7 @@ namespace Ryujinx.Ava
|
||||
Logger.Notice.Print(LogClass.Application, $"Ryujinx Version: {Version}");
|
||||
SystemInfo.Gather().Print();
|
||||
|
||||
var enabledLogs = Logger.GetEnabledLevels();
|
||||
Logger.Notice.Print(LogClass.Application, $"Logs Enabled: {(enabledLogs.Count == 0 ? "<None>" : string.Join(", ", enabledLogs))}");
|
||||
Logger.Notice.Print(LogClass.Application, $"Logs Enabled: {(Logger.GetEnabledLevels().Count == 0 ? "<None>" : string.Join(", ", Logger.GetEnabledLevels()))}");
|
||||
|
||||
if (AppDataManager.Mode == AppDataManager.LaunchMode.Custom)
|
||||
{
|
||||
|
@ -18,26 +18,26 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="0.10.15" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="0.10.15" />
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.15" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="0.10.15" />
|
||||
<PackageReference Include="Avalonia.Markup.Xaml.Loader" Version="0.10.15" />
|
||||
<PackageReference Include="Avalonia.Svg" Version="0.10.14" />
|
||||
<PackageReference Include="Avalonia.Svg.Skia" Version="0.10.14" />
|
||||
<PackageReference Include="Avalonia" Version="0.10.18" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="0.10.18" />
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.18" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="0.10.18" />
|
||||
<PackageReference Include="Avalonia.Markup.Xaml.Loader" Version="0.10.18" />
|
||||
<PackageReference Include="Avalonia.Svg" Version="0.10.18" />
|
||||
<PackageReference Include="Avalonia.Svg.Skia" Version="0.10.18" />
|
||||
<PackageReference Include="jp2masa.Avalonia.Flexbox" Version="0.2.0" />
|
||||
<PackageReference Include="DynamicData" Version="7.9.4" />
|
||||
<PackageReference Include="FluentAvaloniaUI" Version="1.4.1" />
|
||||
<PackageReference Include="XamlNameReferenceGenerator" Version="1.3.4" />
|
||||
<PackageReference Include="DynamicData" Version="7.12.8" />
|
||||
<PackageReference Include="FluentAvaloniaUI" Version="1.4.5" />
|
||||
<PackageReference Include="XamlNameReferenceGenerator" Version="1.4.2" />
|
||||
|
||||
<PackageReference Include="OpenTK.Core" Version="4.7.2" />
|
||||
<PackageReference Include="OpenTK.Core" Version="4.7.5" />
|
||||
<PackageReference Include="Ryujinx.Audio.OpenAL.Dependencies" Version="1.21.0.1" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'osx-x64'" />
|
||||
<PackageReference Include="Ryujinx.Graphics.Nvdec.Dependencies" Version="5.0.1-build10" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'osx-x64'" />
|
||||
<PackageReference Include="Silk.NET.Vulkan" Version="2.10.1" />
|
||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.10.1" />
|
||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.10.1" />
|
||||
<PackageReference Include="Silk.NET.Vulkan" Version="2.16.0" />
|
||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.16.0" />
|
||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.16.0" />
|
||||
<PackageReference Include="SPB" Version="0.0.4-build28" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.3.3" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.4.1" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@ -57,14 +57,14 @@ namespace Ryujinx.Ava.Ui.Applet
|
||||
|
||||
bool opened = false;
|
||||
|
||||
UserResult response = await ContentDialogHelper.ShowDeferredContentDialog(_parent,
|
||||
title,
|
||||
message,
|
||||
"",
|
||||
LocaleManager.Instance["DialogOpenSettingsWindowLabel"],
|
||||
"",
|
||||
LocaleManager.Instance["SettingsButtonClose"],
|
||||
(int)Symbol.Important,
|
||||
UserResult response = await ContentDialogHelper.ShowDeferredContentDialog(_parent,
|
||||
title,
|
||||
message,
|
||||
"",
|
||||
LocaleManager.Instance["DialogOpenSettingsWindowLabel"],
|
||||
"",
|
||||
LocaleManager.Instance["SettingsButtonClose"],
|
||||
(int)Symbol.Important,
|
||||
deferEvent,
|
||||
async (window) =>
|
||||
{
|
||||
@ -168,7 +168,7 @@ namespace Ryujinx.Ava.Ui.Applet
|
||||
|
||||
object response = await msgDialog.Run();
|
||||
|
||||
if (response != null && buttons.Length > 1 && (int)response != buttons.Length - 1)
|
||||
if (response != null && buttons != null && buttons.Length > 1 && (int)response != buttons.Length - 1)
|
||||
{
|
||||
showDetails = true;
|
||||
}
|
||||
|
@ -127,9 +127,16 @@ namespace Ryujinx.Ava.Ui.Controls
|
||||
contentDialog.PrimaryButtonClick += deferCloseAction;
|
||||
}
|
||||
|
||||
await contentDialog.ShowAsync(ContentDialogPlacement.Popup);
|
||||
if (useOverlay)
|
||||
{
|
||||
await contentDialog.ShowAsync(overlay, ContentDialogPlacement.Popup);
|
||||
|
||||
overlay?.Close();
|
||||
overlay!.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
await contentDialog.ShowAsync(ContentDialogPlacement.Popup);
|
||||
}
|
||||
}
|
||||
|
||||
if (useOverlay)
|
||||
@ -391,4 +398,4 @@ namespace Ryujinx.Ava.Ui.Controls
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -6,8 +6,8 @@ using SPB.Graphics;
|
||||
using SPB.Platform;
|
||||
using SPB.Platform.GLX;
|
||||
using SPB.Platform.X11;
|
||||
using SPB.Windowing;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading.Tasks;
|
||||
@ -15,12 +15,12 @@ using static Ryujinx.Ava.Ui.Controls.Win32NativeInterop;
|
||||
|
||||
namespace Ryujinx.Ava.Ui.Controls
|
||||
{
|
||||
public unsafe class EmbeddedWindow : NativeControlHost
|
||||
public class EmbeddedWindow : NativeControlHost
|
||||
{
|
||||
private WindowProc _wndProcDelegate;
|
||||
private string _className;
|
||||
|
||||
protected GLXWindow X11Window { get; private set; }
|
||||
protected GLXWindow X11Window { get; set; }
|
||||
protected IntPtr WindowHandle { get; set; }
|
||||
protected IntPtr X11Display { get; set; }
|
||||
|
||||
@ -94,19 +94,17 @@ namespace Ryujinx.Ava.Ui.Controls
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("linux")]
|
||||
IPlatformHandle CreateLinux(IPlatformHandle parent)
|
||||
protected virtual IPlatformHandle CreateLinux(IPlatformHandle parent)
|
||||
{
|
||||
X11Window = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100) as GLXWindow;
|
||||
|
||||
X11Window = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100) as GLXWindow;
|
||||
WindowHandle = X11Window.WindowHandle.RawHandle;
|
||||
|
||||
X11Display = X11Window.DisplayHandle.RawHandle;
|
||||
X11Display = X11Window.DisplayHandle.RawHandle;
|
||||
|
||||
return new PlatformHandle(WindowHandle, "X11");
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
unsafe IPlatformHandle CreateWin32(IPlatformHandle parent)
|
||||
IPlatformHandle CreateWin32(IPlatformHandle parent)
|
||||
{
|
||||
_className = "NativeWindow-" + Guid.NewGuid();
|
||||
_wndProcDelegate = WndProc;
|
||||
@ -142,7 +140,7 @@ namespace Ryujinx.Ava.Ui.Controls
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
internal IntPtr WndProc(IntPtr hWnd, WindowsMessages msg, IntPtr wParam, IntPtr lParam)
|
||||
IntPtr WndProc(IntPtr hWnd, WindowsMessages msg, IntPtr wParam, IntPtr lParam)
|
||||
{
|
||||
var point = new Point((long)lParam & 0xFFFF, ((long)lParam >> 16) & 0xFFFF);
|
||||
var root = VisualRoot as Window;
|
||||
|
@ -49,7 +49,7 @@ namespace Ryujinx.Ava.Ui.Controls
|
||||
{
|
||||
throw new PlatformNotSupportedException();
|
||||
}
|
||||
|
||||
|
||||
var flags = OpenGLContextFlags.Compat;
|
||||
if (_graphicsDebugLevel != GraphicsDebugLevel.None)
|
||||
{
|
||||
@ -69,12 +69,12 @@ namespace Ryujinx.Ava.Ui.Controls
|
||||
|
||||
public void MakeCurrent()
|
||||
{
|
||||
Context.MakeCurrent(_window);
|
||||
Context?.MakeCurrent(_window);
|
||||
}
|
||||
|
||||
public void MakeCurrent(NativeWindowBase window)
|
||||
{
|
||||
Context.MakeCurrent(window);
|
||||
Context?.MakeCurrent(window);
|
||||
}
|
||||
|
||||
public void SwapBuffers()
|
||||
|
@ -1,100 +0,0 @@
|
||||
using Avalonia.Rendering;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Timers;
|
||||
|
||||
namespace Ryujinx.Ava.Ui.Controls
|
||||
{
|
||||
internal class RenderTimer : IRenderTimer, IDisposable
|
||||
{
|
||||
public event Action<TimeSpan> Tick
|
||||
{
|
||||
add
|
||||
{
|
||||
_tick += value;
|
||||
|
||||
if (_subscriberCount++ == 0)
|
||||
{
|
||||
Start();
|
||||
}
|
||||
}
|
||||
|
||||
remove
|
||||
{
|
||||
if (--_subscriberCount == 0)
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
_tick -= value;
|
||||
}
|
||||
}
|
||||
|
||||
private Thread _tickThread;
|
||||
private readonly System.Timers.Timer _timer;
|
||||
|
||||
private Action<TimeSpan> _tick;
|
||||
private int _subscriberCount;
|
||||
|
||||
private bool _isRunning;
|
||||
|
||||
private AutoResetEvent _resetEvent;
|
||||
|
||||
public RenderTimer()
|
||||
{
|
||||
_timer = new System.Timers.Timer(15);
|
||||
_resetEvent = new AutoResetEvent(true);
|
||||
_timer.Elapsed += Timer_Elapsed;
|
||||
}
|
||||
|
||||
private void Timer_Elapsed(object sender, ElapsedEventArgs e)
|
||||
{
|
||||
TickNow();
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_timer.Start();
|
||||
if (_tickThread == null)
|
||||
{
|
||||
_tickThread = new Thread(RunTick);
|
||||
_tickThread.Name = "RenderTimerTickThread";
|
||||
_tickThread.IsBackground = true;
|
||||
_isRunning = true;
|
||||
_tickThread.Start();
|
||||
}
|
||||
}
|
||||
|
||||
public void RunTick()
|
||||
{
|
||||
while (_isRunning)
|
||||
{
|
||||
_resetEvent.WaitOne();
|
||||
_tick?.Invoke(TimeSpan.FromMilliseconds(Environment.TickCount));
|
||||
}
|
||||
}
|
||||
|
||||
public void TickNow()
|
||||
{
|
||||
lock (_timer)
|
||||
{
|
||||
_resetEvent.Set();
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_timer.Stop();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_timer.Elapsed -= Timer_Elapsed;
|
||||
_timer.Stop();
|
||||
_isRunning = false;
|
||||
_resetEvent.Set();
|
||||
_tickThread.Join();
|
||||
_resetEvent.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
@ -4,11 +4,4 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Ryujinx.Ava.Ui.Controls.RendererHost">
|
||||
<ContentControl
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
Name="View"
|
||||
/>
|
||||
</UserControl>
|
||||
|
@ -41,7 +41,7 @@ namespace Ryujinx.Ava.Ui.Controls
|
||||
{
|
||||
_currentWindow.WindowCreated += CurrentWindow_WindowCreated;
|
||||
_currentWindow.SizeChanged += CurrentWindow_SizeChanged;
|
||||
View.Content = _currentWindow;
|
||||
Content = _currentWindow;
|
||||
}
|
||||
|
||||
public void CreateVulkan()
|
||||
|
@ -1,10 +1,13 @@
|
||||
using Avalonia.Platform;
|
||||
using Ryujinx.Ava.Ui.Controls;
|
||||
using Silk.NET.Vulkan;
|
||||
using SPB.Graphics.Vulkan;
|
||||
using SPB.Platform.GLX;
|
||||
using SPB.Platform.Win32;
|
||||
using SPB.Platform.X11;
|
||||
using SPB.Windowing;
|
||||
using System;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace Ryujinx.Ava.Ui
|
||||
{
|
||||
@ -12,6 +15,18 @@ namespace Ryujinx.Ava.Ui
|
||||
{
|
||||
private NativeWindowBase _window;
|
||||
|
||||
[SupportedOSPlatform("linux")]
|
||||
protected override IPlatformHandle CreateLinux(IPlatformHandle parent)
|
||||
{
|
||||
X11Window = new GLXWindow(new NativeHandle(X11.DefaultDisplay), new NativeHandle(parent.Handle));
|
||||
WindowHandle = X11Window.WindowHandle.RawHandle;
|
||||
X11Display = X11Window.DisplayHandle.RawHandle;
|
||||
|
||||
X11Window.Hide();
|
||||
|
||||
return new PlatformHandle(WindowHandle, "X11");
|
||||
}
|
||||
|
||||
public SurfaceKHR CreateSurface(Instance instance)
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
@ -20,7 +35,7 @@ namespace Ryujinx.Ava.Ui
|
||||
}
|
||||
else if (OperatingSystem.IsLinux())
|
||||
{
|
||||
_window = X11Window;
|
||||
_window = new SimpleX11Window(new NativeHandle(X11Display), new NativeHandle(WindowHandle));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -1,8 +1,22 @@
|
||||
namespace Ryujinx.Ava.Ui.Models
|
||||
using Ryujinx.Ava.Ui.ViewModels;
|
||||
|
||||
namespace Ryujinx.Ava.Ui.Models
|
||||
{
|
||||
public class DownloadableContentModel
|
||||
public class DownloadableContentModel : BaseModel
|
||||
{
|
||||
public bool Enabled { get; set; }
|
||||
private bool _enabled;
|
||||
|
||||
public bool Enabled
|
||||
{
|
||||
get => _enabled;
|
||||
set
|
||||
{
|
||||
_enabled = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string TitleId { get; }
|
||||
public string ContainerPath { get; }
|
||||
public string FullPath { get; }
|
||||
|
@ -8,6 +8,7 @@ using Ryujinx.Ava.Ui.Models;
|
||||
using Ryujinx.Ava.Ui.Windows;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
@ -189,7 +190,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
{
|
||||
amiiboJsonString = File.ReadAllText(_amiiboJsonPath);
|
||||
|
||||
if (await NeedsUpdate(JsonSerializer.Deserialize<Amiibo.AmiiboJson>(amiiboJsonString).LastUpdated))
|
||||
if (await NeedsUpdate(JsonHelper.Deserialize<Amiibo.AmiiboJson>(amiiboJsonString).LastUpdated))
|
||||
{
|
||||
amiiboJsonString = await DownloadAmiiboJson();
|
||||
}
|
||||
@ -206,7 +207,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
_amiiboList = JsonSerializer.Deserialize<Amiibo.AmiiboJson>(amiiboJsonString).Amiibo;
|
||||
_amiiboList = JsonHelper.Deserialize<Amiibo.AmiiboJson>(amiiboJsonString).Amiibo;
|
||||
_amiiboList = _amiiboList.OrderBy(amiibo => amiibo.AmiiboSeries).ToList();
|
||||
|
||||
ParseAmiiboData();
|
||||
|
@ -460,8 +460,6 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
{
|
||||
using IGamepad gamepad = _mainWindow.InputManager.KeyboardDriver.GetGamepad(id);
|
||||
|
||||
Logger.Info?.Print(LogClass.Configuration, $"{GetShortGamepadName(gamepad.Name)} has been connected with ID: {gamepad.Id}");
|
||||
|
||||
if (gamepad != null)
|
||||
{
|
||||
Devices.Add((DeviceType.Keyboard, id, $"{GetShortGamepadName(gamepad.Name)}"));
|
||||
@ -472,8 +470,6 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
{
|
||||
using IGamepad gamepad = _mainWindow.InputManager.GamepadDriver.GetGamepad(id);
|
||||
|
||||
Logger.Info?.Print(LogClass.Configuration, $"{GetShortGamepadName(gamepad.Name)} has been connected with ID: {gamepad.Id}");
|
||||
|
||||
if (gamepad != null)
|
||||
{
|
||||
if (Devices.Any(controller => GetShortGamepadId(controller.Id) == GetShortGamepadId(gamepad.Id)))
|
||||
|
@ -19,6 +19,7 @@ using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.HOS;
|
||||
using Ryujinx.Modules;
|
||||
using Ryujinx.Ui.App.Common;
|
||||
using Ryujinx.Ui.Common;
|
||||
@ -47,6 +48,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
private string _loadHeading;
|
||||
private string _cacheLoadStatus;
|
||||
private string _searchText;
|
||||
private Timer _searchTimer;
|
||||
private string _dockedStatusText;
|
||||
private string _fifoStatusText;
|
||||
private string _gameStatusText;
|
||||
@ -115,10 +117,20 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
{
|
||||
_searchText = value;
|
||||
|
||||
RefreshView();
|
||||
_searchTimer?.Dispose();
|
||||
|
||||
_searchTimer = new Timer(TimerCallback, null, 1000, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void TimerCallback(object obj)
|
||||
{
|
||||
RefreshView();
|
||||
|
||||
_searchTimer.Dispose();
|
||||
_searchTimer = null;
|
||||
}
|
||||
|
||||
public ReadOnlyObservableCollection<ApplicationData> AppsObservableList
|
||||
{
|
||||
get => _appsObservableList;
|
||||
@ -200,22 +212,19 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
private string _showUikey = "F4";
|
||||
private string _pauseKey = "F5";
|
||||
private string _screenshotkey = "F8";
|
||||
private float _volume;
|
||||
private float _volume;
|
||||
private string _backendText;
|
||||
|
||||
public ApplicationData SelectedApplication
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (Glyph)
|
||||
return Glyph switch
|
||||
{
|
||||
case Glyph.List:
|
||||
return _owner.GameList.SelectedApplication;
|
||||
case Glyph.Grid:
|
||||
return _owner.GameGrid.SelectedApplication;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
Glyph.List => _owner.GameList.SelectedApplication,
|
||||
Glyph.Grid => _owner.GameGrid.SelectedApplication,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -408,6 +417,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
{
|
||||
_owner.AppHost.Device.SetVolume(_volume);
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(VolumeStatusText));
|
||||
OnPropertyChanged(nameof(VolumeMuted));
|
||||
OnPropertyChanged();
|
||||
@ -477,38 +487,36 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
internal void Sort(bool isAscending)
|
||||
{
|
||||
IsAscending = isAscending;
|
||||
|
||||
RefreshView();
|
||||
}
|
||||
|
||||
internal void Sort(ApplicationSort sort)
|
||||
{
|
||||
SortMode = sort;
|
||||
|
||||
RefreshView();
|
||||
}
|
||||
|
||||
private IComparer<ApplicationData> GetComparer()
|
||||
{
|
||||
switch (SortMode)
|
||||
return SortMode switch
|
||||
{
|
||||
case ApplicationSort.LastPlayed:
|
||||
return new Models.Generic.LastPlayedSortComparer(IsAscending);
|
||||
case ApplicationSort.FileSize:
|
||||
return new Models.Generic.FileSizeSortComparer(IsAscending);
|
||||
case ApplicationSort.TotalTimePlayed:
|
||||
return new Models.Generic.TimePlayedSortComparer(IsAscending);
|
||||
case ApplicationSort.Title:
|
||||
return IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.TitleName) : SortExpressionComparer<ApplicationData>.Descending(app => app.TitleName);
|
||||
case ApplicationSort.Favorite:
|
||||
return !IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Favorite) : SortExpressionComparer<ApplicationData>.Descending(app => app.Favorite);
|
||||
case ApplicationSort.Developer:
|
||||
return IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Developer) : SortExpressionComparer<ApplicationData>.Descending(app => app.Developer);
|
||||
case ApplicationSort.FileType:
|
||||
return IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.FileExtension) : SortExpressionComparer<ApplicationData>.Descending(app => app.FileExtension);
|
||||
case ApplicationSort.Path:
|
||||
return IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Path) : SortExpressionComparer<ApplicationData>.Descending(app => app.Path);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
ApplicationSort.LastPlayed => new Models.Generic.LastPlayedSortComparer(IsAscending),
|
||||
ApplicationSort.FileSize => new Models.Generic.FileSizeSortComparer(IsAscending),
|
||||
ApplicationSort.TotalTimePlayed => new Models.Generic.TimePlayedSortComparer(IsAscending),
|
||||
ApplicationSort.Title => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.TitleName)
|
||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.TitleName),
|
||||
ApplicationSort.Favorite => !IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Favorite)
|
||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.Favorite),
|
||||
ApplicationSort.Developer => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Developer)
|
||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.Developer),
|
||||
ApplicationSort.FileType => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.FileExtension)
|
||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.FileExtension),
|
||||
ApplicationSort.Path => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Path)
|
||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.Path),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
private void RefreshView()
|
||||
@ -611,40 +619,31 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSortedByFavorite => SortMode == ApplicationSort.Favorite;
|
||||
public bool IsSortedByTitle => SortMode == ApplicationSort.Title;
|
||||
public bool IsSortedByDeveloper => SortMode == ApplicationSort.Developer;
|
||||
public bool IsSortedByFavorite => SortMode == ApplicationSort.Favorite;
|
||||
public bool IsSortedByTitle => SortMode == ApplicationSort.Title;
|
||||
public bool IsSortedByDeveloper => SortMode == ApplicationSort.Developer;
|
||||
public bool IsSortedByLastPlayed => SortMode == ApplicationSort.LastPlayed;
|
||||
public bool IsSortedByTimePlayed => SortMode == ApplicationSort.TotalTimePlayed;
|
||||
public bool IsSortedByType => SortMode == ApplicationSort.FileType;
|
||||
public bool IsSortedBySize => SortMode == ApplicationSort.FileSize;
|
||||
public bool IsSortedByPath => SortMode == ApplicationSort.Path;
|
||||
public bool IsSortedByType => SortMode == ApplicationSort.FileType;
|
||||
public bool IsSortedBySize => SortMode == ApplicationSort.FileSize;
|
||||
public bool IsSortedByPath => SortMode == ApplicationSort.Path;
|
||||
|
||||
public string SortName
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (SortMode)
|
||||
return SortMode switch
|
||||
{
|
||||
case ApplicationSort.Title:
|
||||
return LocaleManager.Instance["GameListHeaderApplication"];
|
||||
case ApplicationSort.Developer:
|
||||
return LocaleManager.Instance["GameListHeaderDeveloper"];
|
||||
case ApplicationSort.LastPlayed:
|
||||
return LocaleManager.Instance["GameListHeaderLastPlayed"];
|
||||
case ApplicationSort.TotalTimePlayed:
|
||||
return LocaleManager.Instance["GameListHeaderTimePlayed"];
|
||||
case ApplicationSort.FileType:
|
||||
return LocaleManager.Instance["GameListHeaderFileExtension"];
|
||||
case ApplicationSort.FileSize:
|
||||
return LocaleManager.Instance["GameListHeaderFileSize"];
|
||||
case ApplicationSort.Path:
|
||||
return LocaleManager.Instance["GameListHeaderPath"];
|
||||
case ApplicationSort.Favorite:
|
||||
return LocaleManager.Instance["CommonFavorite"];
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
ApplicationSort.Title => LocaleManager.Instance["GameListHeaderApplication"],
|
||||
ApplicationSort.Developer => LocaleManager.Instance["GameListHeaderDeveloper"],
|
||||
ApplicationSort.LastPlayed => LocaleManager.Instance["GameListHeaderLastPlayed"],
|
||||
ApplicationSort.TotalTimePlayed => LocaleManager.Instance["GameListHeaderTimePlayed"],
|
||||
ApplicationSort.FileType => LocaleManager.Instance["GameListHeaderFileExtension"],
|
||||
ApplicationSort.FileSize => LocaleManager.Instance["GameListHeaderFileSize"],
|
||||
ApplicationSort.Path => LocaleManager.Instance["GameListHeaderPath"],
|
||||
ApplicationSort.Favorite => LocaleManager.Instance["CommonFavorite"],
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -668,6 +667,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
get => KeyGesture.Parse(_showUikey); set
|
||||
{
|
||||
_showUikey = value.ToString();
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
@ -677,6 +677,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
get => KeyGesture.Parse(_screenshotkey); set
|
||||
{
|
||||
_screenshotkey = value.ToString();
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
@ -686,14 +687,15 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
get => KeyGesture.Parse(_pauseKey); set
|
||||
{
|
||||
_pauseKey = value.ToString();
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsGridSmall => ConfigurationState.Instance.Ui.GridSize == 1;
|
||||
public bool IsGridSmall => ConfigurationState.Instance.Ui.GridSize == 1;
|
||||
public bool IsGridMedium => ConfigurationState.Instance.Ui.GridSize == 2;
|
||||
public bool IsGridLarge => ConfigurationState.Instance.Ui.GridSize == 3;
|
||||
public bool IsGridHuge => ConfigurationState.Instance.Ui.GridSize == 4;
|
||||
public bool IsGridLarge => ConfigurationState.Instance.Ui.GridSize == 3;
|
||||
public bool IsGridHuge => ConfigurationState.Instance.Ui.GridSize == 4;
|
||||
|
||||
public int GridSizeScale
|
||||
{
|
||||
@ -728,14 +730,14 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
|
||||
if (_owner.AppHost.Device.System.SearchingForAmiibo(out int deviceId))
|
||||
{
|
||||
string titleId = _owner.AppHost.Device.Application.TitleIdText.ToUpper();
|
||||
string titleId = _owner.AppHost.Device.Application.TitleIdText.ToUpper();
|
||||
AmiiboWindow window = new(_showAll, _lastScannedAmiiboId, titleId);
|
||||
|
||||
await window.ShowDialog(_owner);
|
||||
|
||||
if (window.IsScanned)
|
||||
{
|
||||
_showAll = window.ViewModel.ShowAllAmiibo;
|
||||
_showAll = window.ViewModel.ShowAllAmiibo;
|
||||
_lastScannedAmiiboId = window.ScannedAmiibo.GetId();
|
||||
|
||||
_owner.AppHost.Device.System.ScanAmiibo(deviceId, _lastScannedAmiiboId, window.ViewModel.UseRandomUuid);
|
||||
@ -766,8 +768,9 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
|
||||
private void ApplicationLibrary_ApplicationCountUpdated(object sender, ApplicationCountUpdatedEventArgs e)
|
||||
{
|
||||
StatusBarProgressValue = e.NumAppsLoaded;
|
||||
StatusBarProgressValue = e.NumAppsLoaded;
|
||||
StatusBarProgressMaximum = e.NumAppsFound;
|
||||
|
||||
LocaleManager.Instance.UpdateDynamicValue("StatusBarGamesLoaded", StatusBarProgressValue, StatusBarProgressMaximum);
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
@ -792,9 +795,11 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
Applications.Clear();
|
||||
|
||||
_owner.LoadProgressBar.IsVisible = true;
|
||||
StatusBarProgressMaximum = 0;
|
||||
StatusBarProgressValue = 0;
|
||||
StatusBarProgressMaximum = 0;
|
||||
StatusBarProgressValue = 0;
|
||||
|
||||
LocaleManager.Instance.UpdateDynamicValue("StatusBarGamesLoaded", 0, 0);
|
||||
});
|
||||
|
||||
@ -842,12 +847,12 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
}
|
||||
});
|
||||
|
||||
dialog.Filters.Add(new FileDialogFilter { Name = "NSP", Extensions = { "nsp" } });
|
||||
dialog.Filters.Add(new FileDialogFilter { Name = "NSP", Extensions = { "nsp" } });
|
||||
dialog.Filters.Add(new FileDialogFilter { Name = "PFS0", Extensions = { "pfs0" } });
|
||||
dialog.Filters.Add(new FileDialogFilter { Name = "XCI", Extensions = { "xci" } });
|
||||
dialog.Filters.Add(new FileDialogFilter { Name = "NCA", Extensions = { "nca" } });
|
||||
dialog.Filters.Add(new FileDialogFilter { Name = "NRO", Extensions = { "nro" } });
|
||||
dialog.Filters.Add(new FileDialogFilter { Name = "NSO", Extensions = { "nso" } });
|
||||
dialog.Filters.Add(new FileDialogFilter { Name = "XCI", Extensions = { "xci" } });
|
||||
dialog.Filters.Add(new FileDialogFilter { Name = "NCA", Extensions = { "nca" } });
|
||||
dialog.Filters.Add(new FileDialogFilter { Name = "NRO", Extensions = { "nro" } });
|
||||
dialog.Filters.Add(new FileDialogFilter { Name = "NSO", Extensions = { "nso" } });
|
||||
|
||||
string[] files = await dialog.ShowAsync(_owner);
|
||||
|
||||
@ -878,10 +883,12 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
{
|
||||
ShowUiKey = new KeyGesture(showUiKey, KeyModifiers.None);
|
||||
}
|
||||
|
||||
if (AvaloniaMappingHelper.TryGetAvaKey((Ryujinx.Input.Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Screenshot, out var screenshotKey))
|
||||
{
|
||||
ScreenshotKey = new KeyGesture(screenshotKey, KeyModifiers.None);
|
||||
}
|
||||
|
||||
if (AvaloniaMappingHelper.TryGetAvaKey((Ryujinx.Input.Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Pause, out var pauseKey))
|
||||
{
|
||||
PauseKey = new KeyGesture(pauseKey, KeyModifiers.None);
|
||||
@ -941,9 +948,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
|
||||
_lastFullscreenToggle = Environment.TickCount64;
|
||||
|
||||
WindowState state = _owner.WindowState;
|
||||
|
||||
if (state == WindowState.FullScreen)
|
||||
if (_owner.WindowState == WindowState.FullScreen)
|
||||
{
|
||||
_owner.WindowState = WindowState.Normal;
|
||||
|
||||
@ -971,8 +976,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
{
|
||||
if (IsGameRunning)
|
||||
{
|
||||
ConfigurationState.Instance.System.EnableDockedMode.Value =
|
||||
!ConfigurationState.Instance.System.EnableDockedMode.Value;
|
||||
ConfigurationState.Instance.System.EnableDockedMode.Value = !ConfigurationState.Instance.System.EnableDockedMode.Value;
|
||||
}
|
||||
}
|
||||
|
||||
@ -985,6 +989,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
else if (IsGameRunning)
|
||||
{
|
||||
await Task.Delay(100);
|
||||
|
||||
_owner.AppHost?.ShowExitPrompt();
|
||||
}
|
||||
}
|
||||
@ -994,6 +999,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
_owner.SettingsWindow = new(_owner.VirtualFileSystem, _owner.ContentManager);
|
||||
|
||||
await _owner.SettingsWindow.ShowDialog(_owner);
|
||||
|
||||
LoadConfigurableHotKeys();
|
||||
}
|
||||
|
||||
@ -1004,9 +1010,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
|
||||
public async void OpenAboutWindow()
|
||||
{
|
||||
AboutWindow window = new();
|
||||
|
||||
await window.ShowDialog(_owner);
|
||||
await new AboutWindow().ShowDialog(_owner);
|
||||
}
|
||||
|
||||
public void ChangeLanguage(object obj)
|
||||
@ -1020,7 +1024,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
try
|
||||
{
|
||||
ProgressMaximum = total;
|
||||
ProgressValue = current;
|
||||
ProgressValue = current;
|
||||
|
||||
switch (state)
|
||||
{
|
||||
@ -1030,13 +1034,13 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
{
|
||||
case PtcLoadingState.Start:
|
||||
case PtcLoadingState.Loading:
|
||||
LoadHeading = LocaleManager.Instance["CompilingPPTC"];
|
||||
LoadHeading = LocaleManager.Instance["CompilingPPTC"];
|
||||
IsLoadingIndeterminate = false;
|
||||
break;
|
||||
case PtcLoadingState.Loaded:
|
||||
LoadHeading = string.Format(LocaleManager.Instance["LoadingHeading"], TitleName);
|
||||
LoadHeading = string.Format(LocaleManager.Instance["LoadingHeading"], TitleName);
|
||||
IsLoadingIndeterminate = true;
|
||||
CacheLoadStatus = "";
|
||||
CacheLoadStatus = "";
|
||||
break;
|
||||
}
|
||||
break;
|
||||
@ -1046,13 +1050,13 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
{
|
||||
case ShaderCacheLoadingState.Start:
|
||||
case ShaderCacheLoadingState.Loading:
|
||||
LoadHeading = LocaleManager.Instance["CompilingShaders"];
|
||||
LoadHeading = LocaleManager.Instance["CompilingShaders"];
|
||||
IsLoadingIndeterminate = false;
|
||||
break;
|
||||
case ShaderCacheLoadingState.Loaded:
|
||||
LoadHeading = string.Format(LocaleManager.Instance["LoadingHeading"], TitleName);
|
||||
LoadHeading = string.Format(LocaleManager.Instance["LoadingHeading"], TitleName);
|
||||
IsLoadingIndeterminate = true;
|
||||
CacheLoadStatus = "";
|
||||
CacheLoadStatus = "";
|
||||
break;
|
||||
}
|
||||
break;
|
||||
@ -1065,14 +1069,12 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
|
||||
public void OpenUserSaveDirectory()
|
||||
{
|
||||
var selection = SelectedApplication;
|
||||
|
||||
ApplicationData selection = SelectedApplication;
|
||||
if (selection != null)
|
||||
{
|
||||
Task.Run(() =>
|
||||
{
|
||||
if (!ulong.TryParse(selection.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture,
|
||||
out ulong titleIdNumber))
|
||||
if (!ulong.TryParse(selection.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber))
|
||||
{
|
||||
Dispatcher.UIThread.Post(async () =>
|
||||
{
|
||||
@ -1082,8 +1084,8 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
return;
|
||||
}
|
||||
|
||||
var userId = new LibHac.Fs.UserId((ulong)_owner.AccountManager.LastOpenedUser.UserId.High, (ulong)_owner.AccountManager.LastOpenedUser.UserId.Low);
|
||||
var saveDataFilter = SaveDataFilter.Make(titleIdNumber, saveType: default, userId, saveDataId: default, index: default);
|
||||
UserId userId = new((ulong)_owner.AccountManager.LastOpenedUser.UserId.High, (ulong)_owner.AccountManager.LastOpenedUser.UserId.Low);
|
||||
SaveDataFilter saveDataFilter = SaveDataFilter.Make(titleIdNumber, saveType: default, userId, saveDataId: default, index: default);
|
||||
OpenSaveDirectory(in saveDataFilter, selection, titleIdNumber);
|
||||
});
|
||||
}
|
||||
@ -1091,8 +1093,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
|
||||
public void ToggleFavorite()
|
||||
{
|
||||
var selection = SelectedApplication;
|
||||
|
||||
ApplicationData selection = SelectedApplication;
|
||||
if (selection != null)
|
||||
{
|
||||
selection.Favorite = !selection.Favorite;
|
||||
@ -1108,11 +1109,10 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
|
||||
public void OpenModsDirectory()
|
||||
{
|
||||
var selection = SelectedApplication;
|
||||
|
||||
ApplicationData selection = SelectedApplication;
|
||||
if (selection != null)
|
||||
{
|
||||
string modsBasePath = _owner.VirtualFileSystem.ModLoader.GetModsBasePath();
|
||||
string modsBasePath = _owner.VirtualFileSystem.ModLoader.GetModsBasePath();
|
||||
string titleModsPath = _owner.VirtualFileSystem.ModLoader.GetTitleDir(modsBasePath, selection.TitleId);
|
||||
|
||||
OpenHelper.OpenFolder(titleModsPath);
|
||||
@ -1121,12 +1121,12 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
|
||||
public void OpenSdModsDirectory()
|
||||
{
|
||||
var selection = SelectedApplication;
|
||||
ApplicationData selection = SelectedApplication;
|
||||
|
||||
if (selection != null)
|
||||
{
|
||||
string sdModsBasePath = _owner.VirtualFileSystem.ModLoader.GetSdModsBasePath();
|
||||
string titleModsPath = _owner.VirtualFileSystem.ModLoader.GetTitleDir(sdModsBasePath, selection.TitleId);
|
||||
string titleModsPath = _owner.VirtualFileSystem.ModLoader.GetTitleDir(sdModsBasePath, selection.TitleId);
|
||||
|
||||
OpenHelper.OpenFolder(titleModsPath);
|
||||
}
|
||||
@ -1134,13 +1134,11 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
|
||||
public void OpenPtcDirectory()
|
||||
{
|
||||
var selection = SelectedApplication;
|
||||
|
||||
ApplicationData selection = SelectedApplication;
|
||||
if (selection != null)
|
||||
{
|
||||
string ptcDir = Path.Combine(AppDataManager.GamesDirPath, selection.TitleId, "cache", "cpu");
|
||||
|
||||
string mainPath = Path.Combine(ptcDir, "0");
|
||||
string ptcDir = Path.Combine(AppDataManager.GamesDirPath, selection.TitleId, "cache", "cpu");
|
||||
string mainPath = Path.Combine(ptcDir, "0");
|
||||
string backupPath = Path.Combine(ptcDir, "1");
|
||||
|
||||
if (!Directory.Exists(ptcDir))
|
||||
@ -1156,16 +1154,18 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
|
||||
public async void PurgePtcCache()
|
||||
{
|
||||
var selection = SelectedApplication;
|
||||
|
||||
ApplicationData selection = SelectedApplication;
|
||||
if (selection != null)
|
||||
{
|
||||
DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, selection.TitleId, "cache", "cpu", "0"));
|
||||
DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, selection.TitleId, "cache", "cpu", "0"));
|
||||
DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, selection.TitleId, "cache", "cpu", "1"));
|
||||
|
||||
// FIXME: Found a way to reproduce the bold effect on the title name (fork?).
|
||||
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance["DialogWarning"],
|
||||
string.Format(LocaleManager.Instance["DialogPPTCDeletionMessage"], selection.TitleName), LocaleManager.Instance["InputDialogYes"], LocaleManager.Instance["InputDialogNo"], LocaleManager.Instance["RyujinxConfirm"]);
|
||||
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance["DialogWarning"],
|
||||
string.Format(LocaleManager.Instance["DialogPPTCDeletionMessage"], selection.TitleName),
|
||||
LocaleManager.Instance["InputDialogYes"],
|
||||
LocaleManager.Instance["InputDialogNo"],
|
||||
LocaleManager.Instance["RyujinxConfirm"]);
|
||||
|
||||
List<FileInfo> cacheFiles = new();
|
||||
|
||||
@ -1198,8 +1198,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
|
||||
public void OpenShaderCacheDirectory()
|
||||
{
|
||||
var selection = SelectedApplication;
|
||||
|
||||
ApplicationData selection = SelectedApplication;
|
||||
if (selection != null)
|
||||
{
|
||||
string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, selection.TitleId, "cache", "shader");
|
||||
@ -1220,18 +1219,20 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
|
||||
public async void PurgeShaderCache()
|
||||
{
|
||||
var selection = SelectedApplication;
|
||||
|
||||
ApplicationData selection = SelectedApplication;
|
||||
if (selection != null)
|
||||
{
|
||||
DirectoryInfo shaderCacheDir = new(Path.Combine(AppDataManager.GamesDirPath, selection.TitleId, "cache", "shader"));
|
||||
|
||||
// FIXME: Found a way to reproduce the bold effect on the title name (fork?).
|
||||
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance["DialogWarning"],
|
||||
string.Format(LocaleManager.Instance["DialogShaderDeletionMessage"], selection.TitleName), LocaleManager.Instance["InputDialogYes"], LocaleManager.Instance["InputDialogNo"], LocaleManager.Instance["RyujinxConfirm"]);
|
||||
string.Format(LocaleManager.Instance["DialogShaderDeletionMessage"], selection.TitleName),
|
||||
LocaleManager.Instance["InputDialogYes"],
|
||||
LocaleManager.Instance["InputDialogNo"],
|
||||
LocaleManager.Instance["RyujinxConfirm"]);
|
||||
|
||||
List<DirectoryInfo> oldCacheDirectories = new List<DirectoryInfo>();
|
||||
List<FileInfo> newCacheFiles = new List<FileInfo>();
|
||||
List<DirectoryInfo> oldCacheDirectories = new();
|
||||
List<FileInfo> newCacheFiles = new();
|
||||
|
||||
if (shaderCacheDir.Exists)
|
||||
{
|
||||
@ -1279,38 +1280,28 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
|
||||
public async void OpenTitleUpdateManager()
|
||||
{
|
||||
var selection = SelectedApplication;
|
||||
|
||||
ApplicationData selection = SelectedApplication;
|
||||
if (selection != null)
|
||||
{
|
||||
TitleUpdateWindow titleUpdateManager =
|
||||
new(_owner.VirtualFileSystem, selection.TitleId, selection.TitleName);
|
||||
|
||||
await titleUpdateManager.ShowDialog(_owner);
|
||||
await new TitleUpdateWindow(_owner.VirtualFileSystem, ulong.Parse(selection.TitleId, NumberStyles.HexNumber), selection.TitleName).ShowDialog(_owner);
|
||||
}
|
||||
}
|
||||
|
||||
public async void OpenDownloadableContentManager()
|
||||
{
|
||||
var selection = SelectedApplication;
|
||||
|
||||
ApplicationData selection = SelectedApplication;
|
||||
if (selection != null)
|
||||
{
|
||||
DownloadableContentManagerWindow downloadableContentManager = new(_owner.VirtualFileSystem, ulong.Parse(selection.TitleId, NumberStyles.HexNumber), selection.TitleName);
|
||||
|
||||
await downloadableContentManager.ShowDialog(_owner);
|
||||
await new DownloadableContentManagerWindow(_owner.VirtualFileSystem, ulong.Parse(selection.TitleId, NumberStyles.HexNumber), selection.TitleName).ShowDialog(_owner);
|
||||
}
|
||||
}
|
||||
|
||||
public async void OpenCheatManager()
|
||||
{
|
||||
var selection = SelectedApplication;
|
||||
|
||||
ApplicationData selection = SelectedApplication;
|
||||
if (selection != null)
|
||||
{
|
||||
CheatWindow cheatManager = new(_owner.VirtualFileSystem, selection.TitleId, selection.TitleName);
|
||||
|
||||
await cheatManager.ShowDialog(_owner);
|
||||
await new CheatWindow(_owner.VirtualFileSystem, selection.TitleId, selection.TitleName).ShowDialog(_owner);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1321,13 +1312,10 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
return;
|
||||
}
|
||||
|
||||
var application = _owner.AppHost.Device.Application;
|
||||
|
||||
ApplicationLoader application = _owner.AppHost.Device.Application;
|
||||
if (application != null)
|
||||
{
|
||||
CheatWindow cheatManager = new(_owner.VirtualFileSystem, application.TitleIdText, application.TitleName);
|
||||
|
||||
await cheatManager.ShowDialog(_owner);
|
||||
await new CheatWindow(_owner.VirtualFileSystem, application.TitleIdText, application.TitleName).ShowDialog(_owner);
|
||||
|
||||
_owner.AppHost.Device.EnableCheats();
|
||||
}
|
||||
@ -1335,14 +1323,12 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
|
||||
public void OpenDeviceSaveDirectory()
|
||||
{
|
||||
var selection = SelectedApplication;
|
||||
|
||||
ApplicationData selection = SelectedApplication;
|
||||
if (selection != null)
|
||||
{
|
||||
Task.Run(() =>
|
||||
{
|
||||
if (!ulong.TryParse(selection.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture,
|
||||
out ulong titleIdNumber))
|
||||
if (!ulong.TryParse(selection.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber))
|
||||
{
|
||||
Dispatcher.UIThread.Post(async () =>
|
||||
{
|
||||
@ -1360,14 +1346,12 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
|
||||
public void OpenBcatSaveDirectory()
|
||||
{
|
||||
var selection = SelectedApplication;
|
||||
|
||||
ApplicationData selection = SelectedApplication;
|
||||
if (selection != null)
|
||||
{
|
||||
Task.Run(() =>
|
||||
{
|
||||
if (!ulong.TryParse(selection.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture,
|
||||
out ulong titleIdNumber))
|
||||
if (!ulong.TryParse(selection.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber))
|
||||
{
|
||||
Dispatcher.UIThread.Post(async () =>
|
||||
{
|
||||
@ -1420,12 +1404,10 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
_owner.Close();
|
||||
}
|
||||
|
||||
private async Task HandleFirmwareInstallation(string path)
|
||||
private async Task HandleFirmwareInstallation(string filename)
|
||||
{
|
||||
try
|
||||
{
|
||||
string filename = path;
|
||||
|
||||
SystemVersion firmwareVersion = _owner.ContentManager.VerifyFirmwarePackage(filename);
|
||||
|
||||
if (firmwareVersion == null)
|
||||
@ -1437,7 +1419,6 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
|
||||
string dialogTitle = string.Format(LocaleManager.Instance["DialogFirmwareInstallerFirmwareInstallTitle"], firmwareVersion.VersionString);
|
||||
|
||||
|
||||
SystemVersion currentVersion = _owner.ContentManager.GetCurrentFirmwareVersion();
|
||||
|
||||
string dialogMessage = string.Format(LocaleManager.Instance["DialogFirmwareInstallerFirmwareInstallMessage"], firmwareVersion.VersionString);
|
||||
@ -1480,11 +1461,12 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
string message = string.Format(LocaleManager.Instance["DialogFirmwareInstallerFirmwareInstallSuccessMessage"], firmwareVersion.VersionString);
|
||||
|
||||
await ContentDialogHelper.CreateInfoDialog(dialogTitle, message, LocaleManager.Instance["InputDialogOk"], "", LocaleManager.Instance["RyujinxInfo"]);
|
||||
|
||||
Logger.Info?.Print(LogClass.Application, message);
|
||||
|
||||
// Purge Applet Cache.
|
||||
|
||||
DirectoryInfo miiEditorCacheFolder = new DirectoryInfo(System.IO.Path.Combine(AppDataManager.GamesDirPath, "0100000000001009", "cache"));
|
||||
DirectoryInfo miiEditorCacheFolder = new DirectoryInfo(Path.Combine(AppDataManager.GamesDirPath, "0100000000001009", "cache"));
|
||||
|
||||
if (miiEditorCacheFolder.Exists)
|
||||
{
|
||||
@ -1514,8 +1496,8 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
catch (LibHac.Common.Keys.MissingKeyException ex)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, ex.ToString());
|
||||
Dispatcher.UIThread.Post(async () => await
|
||||
UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys, _owner));
|
||||
|
||||
Dispatcher.UIThread.Post(async () => await UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys, _owner));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -1527,8 +1509,8 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
{
|
||||
OpenFileDialog dialog = new() { AllowMultiple = false };
|
||||
dialog.Filters.Add(new FileDialogFilter { Name = LocaleManager.Instance["FileDialogAllTypes"], Extensions = { "xci", "zip" } });
|
||||
dialog.Filters.Add(new FileDialogFilter { Name = "XCI", Extensions = { "xci" } });
|
||||
dialog.Filters.Add(new FileDialogFilter { Name = "ZIP", Extensions = { "zip" } });
|
||||
dialog.Filters.Add(new FileDialogFilter { Name = "XCI", Extensions = { "xci" } });
|
||||
dialog.Filters.Add(new FileDialogFilter { Name = "ZIP", Extensions = { "zip" } });
|
||||
|
||||
string[] file = await dialog.ShowAsync(_owner);
|
||||
|
||||
|
@ -3,89 +3,128 @@
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
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="800"
|
||||
Height="500"
|
||||
MinWidth="800"
|
||||
MinHeight="500"
|
||||
MaxWidth="800"
|
||||
MaxHeight="500"
|
||||
SizeToContent="Height"
|
||||
Width="600" MinHeight="500" Height="500"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
MinWidth="600"
|
||||
mc:Ignorable="d">
|
||||
<Grid Name="DownloadableContentGrid" Margin="15">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<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"
|
||||
MaxWidth="500"
|
||||
LineHeight="18"
|
||||
TextWrapping="Wrap"
|
||||
Text="{Binding Heading}"
|
||||
TextAlignment="Center" />
|
||||
<Border
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
<DockPanel
|
||||
Grid.Row="2"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Left">
|
||||
<Button
|
||||
Name="EnableAllButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding EnableAll}">
|
||||
<TextBlock Text="{locale:Locale DlcManagerEnableAllButton}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="DisableAllButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding DisableAll}">
|
||||
<TextBlock Text="{locale:Locale DlcManagerDisableAllButton}" />
|
||||
</Button>
|
||||
</DockPanel>
|
||||
<Border
|
||||
Grid.Row="3"
|
||||
Margin="5"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1">
|
||||
<DataGrid
|
||||
MinHeight="200"
|
||||
HorizontalAlignment="Stretch"
|
||||
<ScrollViewer
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
Items="{Binding DownloadableContents}"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTemplateColumn Width="90">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<CheckBox
|
||||
Width="50"
|
||||
MinWidth="40"
|
||||
HorizontalAlignment="Right"
|
||||
IsChecked="{Binding Enabled}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
<DataGridTemplateColumn.Header>
|
||||
<TextBlock Text="{locale:Locale DlcManagerTableHeadingEnabledLabel}" />
|
||||
</DataGridTemplateColumn.Header>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTextColumn
|
||||
Width="190"
|
||||
Binding="{Binding TitleId}"
|
||||
CanUserResize="True">
|
||||
<DataGridTextColumn.Header>
|
||||
<TextBlock Text="{locale:Locale DlcManagerTableHeadingTitleIdLabel}" />
|
||||
</DataGridTextColumn.Header>
|
||||
</DataGridTextColumn>
|
||||
<DataGridTextColumn
|
||||
Width="*"
|
||||
Binding="{Binding ContainerPath}"
|
||||
CanUserResize="True">
|
||||
<DataGridTextColumn.Header>
|
||||
<TextBlock Text="{locale:Locale DlcManagerTableHeadingContainerPathLabel}" />
|
||||
</DataGridTextColumn.Header>
|
||||
</DataGridTextColumn>
|
||||
<DataGridTextColumn
|
||||
Width="*"
|
||||
Binding="{Binding FullPath}"
|
||||
CanUserResize="True">
|
||||
<DataGridTextColumn.Header>
|
||||
<TextBlock Text="{locale:Locale DlcManagerTableHeadingFullPathLabel}" />
|
||||
</DataGridTextColumn.Header>
|
||||
</DataGridTextColumn>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
<DataGrid
|
||||
Name="DlcDataGrid"
|
||||
MinHeight="200"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
CanUserReorderColumns="False"
|
||||
CanUserResizeColumns="True"
|
||||
CanUserSortColumns="True"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
Items="{Binding _downloadableContents}"
|
||||
SelectionMode="Extended"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<DataGrid.Styles>
|
||||
<Styles>
|
||||
<Style Selector="DataGridCell:nth-child(3), DataGridCell:nth-child(4)">
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left" />
|
||||
</Style>
|
||||
</Styles>
|
||||
<Styles>
|
||||
<Style Selector="DataGridCell:nth-child(1)">
|
||||
<Setter Property="HorizontalAlignment" Value="Right" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Right" />
|
||||
</Style>
|
||||
</Styles>
|
||||
</DataGrid.Styles>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTemplateColumn Width="90">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<CheckBox
|
||||
Width="50"
|
||||
MinWidth="40"
|
||||
HorizontalAlignment="Center"
|
||||
IsChecked="{Binding Enabled}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
<DataGridTemplateColumn.Header>
|
||||
<TextBlock Text="{locale:Locale DlcManagerTableHeadingEnabledLabel}" />
|
||||
</DataGridTemplateColumn.Header>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTextColumn Width="140" Binding="{Binding TitleId}">
|
||||
<DataGridTextColumn.Header>
|
||||
<TextBlock Text="{locale:Locale DlcManagerTableHeadingTitleIdLabel}" />
|
||||
</DataGridTextColumn.Header>
|
||||
</DataGridTextColumn>
|
||||
<DataGridTextColumn Width="280" Binding="{Binding FullPath}">
|
||||
<DataGridTextColumn.Header>
|
||||
<TextBlock Text="{locale:Locale DlcManagerTableHeadingFullPathLabel}" />
|
||||
</DataGridTextColumn.Header>
|
||||
</DataGridTextColumn>
|
||||
<DataGridTextColumn Binding="{Binding ContainerPath}">
|
||||
<DataGridTextColumn.Header>
|
||||
<TextBlock Text="{locale:Locale DlcManagerTableHeadingContainerPathLabel}" />
|
||||
</DataGridTextColumn.Header>
|
||||
</DataGridTextColumn>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
<DockPanel
|
||||
Grid.Row="3"
|
||||
Grid.Row="4"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Stretch">
|
||||
<DockPanel Margin="0" HorizontalAlignment="Left">
|
||||
|
@ -18,6 +18,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Path = System.IO.Path;
|
||||
@ -27,14 +28,13 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
public partial class DownloadableContentManagerWindow : StyleableWindow
|
||||
{
|
||||
private readonly List<DownloadableContentContainer> _downloadableContentContainerList;
|
||||
private readonly string _downloadableContentJsonPath;
|
||||
private readonly string _downloadableContentJsonPath;
|
||||
|
||||
public VirtualFileSystem VirtualFileSystem { get; }
|
||||
public AvaloniaList<DownloadableContentModel> DownloadableContents { get; set; } = new AvaloniaList<DownloadableContentModel>();
|
||||
public ulong TitleId { get; }
|
||||
public string TitleName { get; }
|
||||
private VirtualFileSystem _virtualFileSystem { get; }
|
||||
private AvaloniaList<DownloadableContentModel> _downloadableContents { get; set; }
|
||||
|
||||
public string Heading => string.Format(LocaleManager.Instance["DlcWindowHeading"], TitleName, TitleId.ToString("X16"));
|
||||
private ulong _titleId { get; }
|
||||
private string _titleName { get; }
|
||||
|
||||
public DownloadableContentManagerWindow()
|
||||
{
|
||||
@ -42,14 +42,16 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["DlcWindowTitle"];
|
||||
Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance["DlcWindowTitle"]} - {_titleName} ({_titleId:X16})";
|
||||
}
|
||||
|
||||
public DownloadableContentManagerWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
|
||||
{
|
||||
VirtualFileSystem = virtualFileSystem;
|
||||
TitleId = titleId;
|
||||
TitleName = titleName;
|
||||
_virtualFileSystem = virtualFileSystem;
|
||||
_downloadableContents = new AvaloniaList<DownloadableContentModel>();
|
||||
|
||||
_titleId = titleId;
|
||||
_titleName = titleName;
|
||||
|
||||
_downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json");
|
||||
|
||||
@ -66,9 +68,24 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["DlcWindowTitle"];
|
||||
RemoveButton.IsEnabled = false;
|
||||
|
||||
DlcDataGrid.SelectionChanged += DlcDataGrid_SelectionChanged;
|
||||
|
||||
Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance["DlcWindowTitle"]} - {_titleName} ({_titleId:X16})";
|
||||
|
||||
LoadDownloadableContents();
|
||||
PrintHeading();
|
||||
}
|
||||
|
||||
private void DlcDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
RemoveButton.IsEnabled = (DlcDataGrid.SelectedItems.Count > 0);
|
||||
}
|
||||
|
||||
private void PrintHeading()
|
||||
{
|
||||
Heading.Text = string.Format(LocaleManager.Instance["DlcWindowHeading"], _downloadableContents.Count, _titleName, _titleId.ToString("X16"));
|
||||
}
|
||||
|
||||
private void LoadDownloadableContents()
|
||||
@ -79,23 +96,23 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
{
|
||||
using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath);
|
||||
|
||||
PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage());
|
||||
PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage());
|
||||
|
||||
VirtualFileSystem.ImportTickets(pfs);
|
||||
_virtualFileSystem.ImportTickets(partitionFileSystem);
|
||||
|
||||
foreach (DownloadableContentNca downloadableContentNca in downloadableContentContainer.DownloadableContentNcaList)
|
||||
{
|
||||
using var ncaFile = new UniqueRef<IFile>();
|
||||
using UniqueRef<IFile> ncaFile = new();
|
||||
|
||||
pfs.OpenFile(ref ncaFile.Ref(), downloadableContentNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
partitionFileSystem.OpenFile(ref ncaFile.Ref(), downloadableContentNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
|
||||
Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), downloadableContentContainer.ContainerPath);
|
||||
Nca nca = TryOpenNca(ncaFile.Get.AsStorage(), downloadableContentContainer.ContainerPath);
|
||||
if (nca != null)
|
||||
{
|
||||
DownloadableContents.Add(new DownloadableContentModel(nca.Header.TitleId.ToString("X16"),
|
||||
downloadableContentContainer.ContainerPath,
|
||||
downloadableContentNca.FullPath,
|
||||
downloadableContentNca.Enabled));
|
||||
_downloadableContents.Add(new DownloadableContentModel(nca.Header.TitleId.ToString("X16"),
|
||||
downloadableContentContainer.ContainerPath,
|
||||
downloadableContentNca.FullPath,
|
||||
downloadableContentNca.Enabled));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -105,11 +122,11 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
Save();
|
||||
}
|
||||
|
||||
private Nca TryCreateNca(IStorage ncaStorage, string containerPath)
|
||||
private Nca TryOpenNca(IStorage ncaStorage, string containerPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new Nca(VirtualFileSystem.KeySet, ncaStorage);
|
||||
return new Nca(_virtualFileSystem.KeySet, ncaStorage);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -124,61 +141,73 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
|
||||
private async Task AddDownloadableContent(string path)
|
||||
{
|
||||
if (!File.Exists(path) || DownloadableContents.FirstOrDefault(x => x.ContainerPath == path) != null)
|
||||
if (!File.Exists(path) || _downloadableContents.FirstOrDefault(x => x.ContainerPath == path) != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (FileStream containerFile = File.OpenRead(path))
|
||||
using FileStream containerFile = File.OpenRead(path);
|
||||
|
||||
PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage());
|
||||
bool containsDownloadableContent = false;
|
||||
|
||||
_virtualFileSystem.ImportTickets(partitionFileSystem);
|
||||
|
||||
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
|
||||
{
|
||||
PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage());
|
||||
bool containsDownloadableContent = false;
|
||||
using var ncaFile = new UniqueRef<IFile>();
|
||||
|
||||
VirtualFileSystem.ImportTickets(pfs);
|
||||
partitionFileSystem.OpenFile(ref ncaFile.Ref(), fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
|
||||
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
|
||||
Nca nca = TryOpenNca(ncaFile.Get.AsStorage(), path);
|
||||
if (nca == null)
|
||||
{
|
||||
using var ncaFile = new UniqueRef<IFile>();
|
||||
|
||||
pfs.OpenFile(ref ncaFile.Ref(), fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
|
||||
Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), path);
|
||||
|
||||
if (nca == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nca.Header.ContentType == NcaContentType.PublicData)
|
||||
{
|
||||
if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != TitleId)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
DownloadableContents.Add(new DownloadableContentModel(nca.Header.TitleId.ToString("X16"), path, fileEntry.FullPath, true));
|
||||
|
||||
containsDownloadableContent = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!containsDownloadableContent)
|
||||
if (nca.Header.ContentType == NcaContentType.PublicData)
|
||||
{
|
||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance["DialogDlcNoDlcErrorMessage"]);
|
||||
if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != _titleId)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
_downloadableContents.Add(new DownloadableContentModel(nca.Header.TitleId.ToString("X16"), path, fileEntry.FullPath, true));
|
||||
|
||||
containsDownloadableContent = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!containsDownloadableContent)
|
||||
{
|
||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance["DialogDlcNoDlcErrorMessage"]);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveDownloadableContents(bool removeSelectedOnly = false)
|
||||
{
|
||||
if (removeSelectedOnly)
|
||||
{
|
||||
DownloadableContents.RemoveAll(DownloadableContents.Where(x => x.Enabled).ToList());
|
||||
AvaloniaList<DownloadableContentModel> removedItems = new();
|
||||
|
||||
foreach (var item in DlcDataGrid.SelectedItems)
|
||||
{
|
||||
removedItems.Add(item as DownloadableContentModel);
|
||||
}
|
||||
|
||||
DlcDataGrid.SelectedItems.Clear();
|
||||
|
||||
foreach (var item in removedItems)
|
||||
{
|
||||
_downloadableContents.RemoveAll(_downloadableContents.Where(x => x.TitleId == item.TitleId).ToList());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DownloadableContents.Clear();
|
||||
_downloadableContents.Clear();
|
||||
}
|
||||
|
||||
PrintHeading();
|
||||
}
|
||||
|
||||
public void RemoveSelected()
|
||||
@ -191,6 +220,22 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
RemoveDownloadableContents();
|
||||
}
|
||||
|
||||
public void EnableAll()
|
||||
{
|
||||
foreach(var item in _downloadableContents)
|
||||
{
|
||||
item.Enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void DisableAll()
|
||||
{
|
||||
foreach (var item in _downloadableContents)
|
||||
{
|
||||
item.Enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async void Add()
|
||||
{
|
||||
OpenFileDialog dialog = new OpenFileDialog()
|
||||
@ -214,6 +259,8 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
await AddDownloadableContent(file);
|
||||
}
|
||||
}
|
||||
|
||||
PrintHeading();
|
||||
}
|
||||
|
||||
public void Save()
|
||||
@ -222,7 +269,7 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
|
||||
DownloadableContentContainer container = default;
|
||||
|
||||
foreach (DownloadableContentModel downloadableContent in DownloadableContents)
|
||||
foreach (DownloadableContentModel downloadableContent in _downloadableContents)
|
||||
{
|
||||
if (container.ContainerPath != downloadableContent.ContainerPath)
|
||||
{
|
||||
|
@ -13,7 +13,7 @@
|
||||
Title="Ryujinx"
|
||||
Width="1280"
|
||||
Height="785"
|
||||
MinWidth="1024"
|
||||
MinWidth="1092"
|
||||
MinHeight="680"
|
||||
d:DesignHeight="720"
|
||||
d:DesignWidth="1280"
|
||||
@ -57,6 +57,8 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel
|
||||
Name="MenuBar"
|
||||
MinHeight="35"
|
||||
Grid.Row="0"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
@ -549,6 +551,7 @@
|
||||
<Grid
|
||||
Name="StatusBar"
|
||||
Grid.Row="2"
|
||||
MinHeight="30"
|
||||
Height="30"
|
||||
Margin="0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
|
@ -90,8 +90,10 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
|
||||
Title = $"Ryujinx {Program.Version}";
|
||||
|
||||
Height = Height / Program.WindowScaleFactor;
|
||||
Width = Width / Program.WindowScaleFactor;
|
||||
// NOTE: Height of MenuBar and StatusBar is not usable here, since it would still be 0 at this point.
|
||||
double barHeight = MenuBar.MinHeight + StatusBar.MinHeight;
|
||||
Height = ((Height - barHeight) / Program.WindowScaleFactor) + barHeight;
|
||||
Width /= Program.WindowScaleFactor;
|
||||
|
||||
if (Program.PreviewerDetached)
|
||||
{
|
||||
@ -251,24 +253,29 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
|
||||
AppHost = new AppHost(RendererControl, InputManager, path, VirtualFileSystem, ContentManager, AccountManager, _userChannelPersistence, this);
|
||||
|
||||
if (!AppHost.LoadGuestApplication().Result)
|
||||
Dispatcher.UIThread.Post(async () =>
|
||||
{
|
||||
AppHost.DisposeContext();
|
||||
if (!await AppHost.LoadGuestApplication())
|
||||
{
|
||||
AppHost.DisposeContext();
|
||||
AppHost = null;
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ViewModel.LoadHeading = string.IsNullOrWhiteSpace(titleName) ? string.Format(LocaleManager.Instance["LoadingHeading"], AppHost.Device.Application.TitleName) : titleName;
|
||||
ViewModel.TitleName = string.IsNullOrWhiteSpace(titleName) ? AppHost.Device.Application.TitleName : titleName;
|
||||
ViewModel.LoadHeading = string.IsNullOrWhiteSpace(titleName) ? string.Format(LocaleManager.Instance["LoadingHeading"], AppHost.Device.Application.TitleName) : titleName;
|
||||
ViewModel.TitleName = string.IsNullOrWhiteSpace(titleName) ? AppHost.Device.Application.TitleName : titleName;
|
||||
|
||||
SwitchToGameControl(startFullscreen);
|
||||
SwitchToGameControl(startFullscreen);
|
||||
|
||||
_currentEmulatedGamePath = path;
|
||||
Thread gameThread = new Thread(InitializeGame)
|
||||
{
|
||||
Name = "GUI.WindowThread"
|
||||
};
|
||||
gameThread.Start();
|
||||
_currentEmulatedGamePath = path;
|
||||
|
||||
Thread gameThread = new(InitializeGame)
|
||||
{
|
||||
Name = "GUI.WindowThread"
|
||||
};
|
||||
gameThread.Start();
|
||||
});
|
||||
}
|
||||
|
||||
private void InitializeGame()
|
||||
@ -518,23 +525,20 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
|
||||
public static void UpdateGraphicsConfig()
|
||||
{
|
||||
int resScale = ConfigurationState.Instance.Graphics.ResScale;
|
||||
float resScaleCustom = ConfigurationState.Instance.Graphics.ResScaleCustom;
|
||||
|
||||
GraphicsConfig.ResScale = resScale == -1 ? resScaleCustom : resScale;
|
||||
GraphicsConfig.MaxAnisotropy = ConfigurationState.Instance.Graphics.MaxAnisotropy;
|
||||
GraphicsConfig.ShadersDumpPath = ConfigurationState.Instance.Graphics.ShadersDumpPath;
|
||||
GraphicsConfig.EnableShaderCache = ConfigurationState.Instance.Graphics.EnableShaderCache;
|
||||
GraphicsConfig.ResScale = ConfigurationState.Instance.Graphics.ResScale == -1 ? ConfigurationState.Instance.Graphics.ResScaleCustom : ConfigurationState.Instance.Graphics.ResScale;
|
||||
GraphicsConfig.MaxAnisotropy = ConfigurationState.Instance.Graphics.MaxAnisotropy;
|
||||
GraphicsConfig.ShadersDumpPath = ConfigurationState.Instance.Graphics.ShadersDumpPath;
|
||||
GraphicsConfig.EnableShaderCache = ConfigurationState.Instance.Graphics.EnableShaderCache;
|
||||
GraphicsConfig.EnableTextureRecompression = ConfigurationState.Instance.Graphics.EnableTextureRecompression;
|
||||
GraphicsConfig.EnableMacroHLE = ConfigurationState.Instance.Graphics.EnableMacroHLE;
|
||||
GraphicsConfig.EnableMacroHLE = ConfigurationState.Instance.Graphics.EnableMacroHLE;
|
||||
}
|
||||
|
||||
public void LoadHotKeys()
|
||||
{
|
||||
HotKeyManager.SetHotKey(FullscreenHotKey, new KeyGesture(Key.Enter, KeyModifiers.Alt));
|
||||
HotKeyManager.SetHotKey(FullscreenHotKey, new KeyGesture(Key.Enter, KeyModifiers.Alt));
|
||||
HotKeyManager.SetHotKey(FullscreenHotKey2, new KeyGesture(Key.F11));
|
||||
HotKeyManager.SetHotKey(DockToggleHotKey, new KeyGesture(Key.F9));
|
||||
HotKeyManager.SetHotKey(ExitHotKey, new KeyGesture(Key.Escape));
|
||||
HotKeyManager.SetHotKey(DockToggleHotKey, new KeyGesture(Key.F9));
|
||||
HotKeyManager.SetHotKey(ExitHotKey, new KeyGesture(Key.Escape));
|
||||
}
|
||||
|
||||
public static void SaveConfig()
|
||||
@ -546,10 +550,12 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
{
|
||||
ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata =>
|
||||
{
|
||||
DateTime lastPlayedDateTime = DateTime.Parse(appMetadata.LastPlayed);
|
||||
double sessionTimePlayed = DateTime.UtcNow.Subtract(lastPlayedDateTime).TotalSeconds;
|
||||
if (DateTime.TryParse(appMetadata.LastPlayed, out DateTime lastPlayedDateTime))
|
||||
{
|
||||
double sessionTimePlayed = DateTime.UtcNow.Subtract(lastPlayedDateTime).TotalSeconds;
|
||||
|
||||
appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero);
|
||||
appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -75,7 +75,7 @@
|
||||
Spacing="10">
|
||||
<ListBox
|
||||
Name="GameList"
|
||||
MinHeight="150"
|
||||
MinHeight="250"
|
||||
Items="{Binding GameDirectories}" />
|
||||
<Grid HorizontalAlignment="Stretch">
|
||||
<Grid.ColumnDefinitions>
|
||||
|
@ -16,7 +16,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using TimeZone = Ryujinx.Ava.Ui.Models.TimeZone;
|
||||
|
||||
namespace Ryujinx.Ava.Ui.Windows
|
||||
@ -31,7 +30,7 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
{
|
||||
Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance["Settings"]}";
|
||||
|
||||
ViewModel = new SettingsViewModel(virtualFileSystem, contentManager, this);
|
||||
ViewModel = new SettingsViewModel(virtualFileSystem, contentManager, this);
|
||||
DataContext = ViewModel;
|
||||
|
||||
InitializeComponent();
|
||||
@ -48,7 +47,7 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
|
||||
public SettingsWindow()
|
||||
{
|
||||
ViewModel = new SettingsViewModel();
|
||||
ViewModel = new SettingsViewModel();
|
||||
DataContext = ViewModel;
|
||||
|
||||
InitializeComponent();
|
||||
@ -79,7 +78,7 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
|
||||
PointerPressed += MouseClick;
|
||||
|
||||
IKeyboard keyboard = (IKeyboard)ViewModel.AvaloniaKeyboardDriver.GetGamepad(ViewModel.AvaloniaKeyboardDriver.GamepadsIds[0]);
|
||||
IKeyboard keyboard = (IKeyboard)ViewModel.AvaloniaKeyboardDriver.GetGamepad(ViewModel.AvaloniaKeyboardDriver.GamepadsIds[0]);
|
||||
IButtonAssigner assigner = new KeyboardKeyAssigner(keyboard);
|
||||
|
||||
_currentAssigner.GetInputAndAssign(assigner);
|
||||
@ -92,6 +91,7 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
|
||||
_currentAssigner.Cancel();
|
||||
_currentAssigner = null;
|
||||
|
||||
button.IsChecked = false;
|
||||
}
|
||||
}
|
||||
@ -122,36 +122,19 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
{
|
||||
if (e.SelectedItem is NavigationViewItem navitem)
|
||||
{
|
||||
switch (navitem.Tag.ToString())
|
||||
NavPanel.Content = navitem.Tag.ToString() switch
|
||||
{
|
||||
case "UiPage":
|
||||
NavPanel.Content = UiPage;
|
||||
break;
|
||||
case "InputPage":
|
||||
NavPanel.Content = InputPage;
|
||||
break;
|
||||
case "HotkeysPage":
|
||||
NavPanel.Content = HotkeysPage;
|
||||
break;
|
||||
case "SystemPage":
|
||||
NavPanel.Content = SystemPage;
|
||||
break;
|
||||
case "CpuPage":
|
||||
NavPanel.Content = CpuPage;
|
||||
break;
|
||||
case "GraphicsPage":
|
||||
NavPanel.Content = GraphicsPage;
|
||||
break;
|
||||
case "AudioPage":
|
||||
NavPanel.Content = AudioPage;
|
||||
break;
|
||||
case "NetworkPage":
|
||||
NavPanel.Content = NetworkPage;
|
||||
break;
|
||||
case "LoggingPage":
|
||||
NavPanel.Content = LoggingPage;
|
||||
break;
|
||||
}
|
||||
"UiPage" => UiPage,
|
||||
"InputPage" => InputPage,
|
||||
"HotkeysPage" => HotkeysPage,
|
||||
"SystemPage" => SystemPage,
|
||||
"CpuPage" => CpuPage,
|
||||
"GraphicsPage" => GraphicsPage,
|
||||
"AudioPage" => AudioPage,
|
||||
"NetworkPage" => NetworkPage,
|
||||
"LoggingPage" => LoggingPage,
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -178,13 +161,18 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
|
||||
private void RemoveButton_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
List<string> selected = new(GameList.SelectedItems.Cast<string>());
|
||||
int oldIndex = GameList.SelectedIndex;
|
||||
|
||||
foreach (string path in selected)
|
||||
foreach (string path in new List<string>(GameList.SelectedItems.Cast<string>()))
|
||||
{
|
||||
ViewModel.GameDirectories.Remove(path);
|
||||
ViewModel.DirectoryChanged = true;
|
||||
}
|
||||
|
||||
if (GameList.ItemCount > 0)
|
||||
{
|
||||
GameList.SelectedIndex = oldIndex < GameList.ItemCount ? oldIndex : 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void TimeZoneBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
@ -214,7 +202,6 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
private void SaveButton_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
SaveSettings();
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
@ -232,7 +219,6 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
private void SaveSettings()
|
||||
{
|
||||
ViewModel.SaveSettings();
|
||||
|
||||
ControllerSettings?.SaveCurrentProfile();
|
||||
|
||||
if (Owner is MainWindow window && ViewModel.DirectoryChanged)
|
||||
@ -246,8 +232,10 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
protected override void OnClosed(EventArgs e)
|
||||
{
|
||||
ControllerSettings.Dispose();
|
||||
|
||||
_currentAssigner?.Cancel();
|
||||
_currentAssigner = null;
|
||||
|
||||
base.OnClosed(e);
|
||||
}
|
||||
}
|
||||
|
@ -3,13 +3,17 @@
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
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"
|
||||
SizeToContent="Height"
|
||||
Width="600" MinHeight="500" Height="500"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Width="600"
|
||||
Height="400"
|
||||
MinWidth="600"
|
||||
MinHeight="400"
|
||||
MaxWidth="600"
|
||||
MaxHeight="400"
|
||||
SizeToContent="Height"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
mc:Ignorable="d">
|
||||
<Grid Margin="15">
|
||||
<Grid.RowDefinitions>
|
||||
@ -19,15 +23,15 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock
|
||||
Name="Heading"
|
||||
Grid.Row="1"
|
||||
MaxWidth="500"
|
||||
Margin="20,15,20,20"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
MaxWidth="500"
|
||||
LineHeight="18"
|
||||
TextWrapping="Wrap"
|
||||
Text="{Binding Heading}"
|
||||
TextAlignment="Center" />
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
<Border
|
||||
Grid.Row="2"
|
||||
Margin="5"
|
||||
@ -36,8 +40,6 @@
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1">
|
||||
<ScrollViewer
|
||||
Width="550"
|
||||
MinHeight="200"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
@ -45,11 +47,19 @@
|
||||
Margin="10"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Items="{Binding TitleUpdates}">
|
||||
Items="{Binding _titleUpdates}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<RadioButton Padding="8, 0" VerticalContentAlignment="Center" GroupName="Update" IsChecked="{Binding IsEnabled, Mode=TwoWay}">
|
||||
<Label Margin="0" VerticalAlignment="Center" Content="{Binding Label}" />
|
||||
<RadioButton
|
||||
Padding="8,0"
|
||||
VerticalContentAlignment="Center"
|
||||
GroupName="Update"
|
||||
IsChecked="{Binding IsEnabled, Mode=TwoWay}">
|
||||
<Label
|
||||
Margin="0"
|
||||
VerticalAlignment="Center"
|
||||
Content="{Binding Label}"
|
||||
FontSize="12" />
|
||||
</RadioButton>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
|
@ -30,13 +30,11 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
private readonly string _titleUpdateJsonPath;
|
||||
private TitleUpdateMetadata _titleUpdateWindowData;
|
||||
|
||||
public VirtualFileSystem VirtualFileSystem { get; }
|
||||
private VirtualFileSystem _virtualFileSystem { get; }
|
||||
private AvaloniaList<TitleUpdateModel> _titleUpdates { get; set; }
|
||||
|
||||
internal AvaloniaList<TitleUpdateModel> TitleUpdates { get; set; } = new AvaloniaList<TitleUpdateModel>();
|
||||
public string TitleId { get; }
|
||||
public string TitleName { get; }
|
||||
|
||||
public string Heading => string.Format(LocaleManager.Instance["GameUpdateWindowHeading"], TitleName, TitleId.ToUpper());
|
||||
private ulong _titleId { get; }
|
||||
private string _titleName { get; }
|
||||
|
||||
public TitleUpdateWindow()
|
||||
{
|
||||
@ -44,16 +42,18 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["UpdateWindowTitle"];
|
||||
Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance["UpdateWindowTitle"]} - {_titleName} ({_titleId:X16})";
|
||||
}
|
||||
|
||||
public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName)
|
||||
public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
|
||||
{
|
||||
VirtualFileSystem = virtualFileSystem;
|
||||
TitleId = titleId;
|
||||
TitleName = titleName;
|
||||
_virtualFileSystem = virtualFileSystem;
|
||||
_titleUpdates = new AvaloniaList<TitleUpdateModel>();
|
||||
|
||||
_titleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId, "updates.json");
|
||||
_titleId = titleId;
|
||||
_titleName = titleName;
|
||||
|
||||
_titleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "updates.json");
|
||||
|
||||
try
|
||||
{
|
||||
@ -64,7 +64,7 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
_titleUpdateWindowData = new TitleUpdateMetadata
|
||||
{
|
||||
Selected = "",
|
||||
Paths = new List<string>()
|
||||
Paths = new List<string>()
|
||||
};
|
||||
}
|
||||
|
||||
@ -72,14 +72,20 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["UpdateWindowTitle"];
|
||||
Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance["UpdateWindowTitle"]} - {_titleName} ({_titleId:X16})";
|
||||
|
||||
LoadUpdates();
|
||||
PrintHeading();
|
||||
}
|
||||
|
||||
private void PrintHeading()
|
||||
{
|
||||
Heading.Text = string.Format(LocaleManager.Instance["GameUpdateWindowHeading"], _titleUpdates.Count, _titleName, _titleId.ToString("X16"));
|
||||
}
|
||||
|
||||
private void LoadUpdates()
|
||||
{
|
||||
TitleUpdates.Add(new TitleUpdateModel(default, string.Empty, true));
|
||||
_titleUpdates.Add(new TitleUpdateModel(default, string.Empty, true));
|
||||
|
||||
foreach (string path in _titleUpdateWindowData.Paths)
|
||||
{
|
||||
@ -88,12 +94,12 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
|
||||
if (_titleUpdateWindowData.Selected == "")
|
||||
{
|
||||
TitleUpdates[0].IsEnabled = true;
|
||||
_titleUpdates[0].IsEnabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
TitleUpdateModel selected = TitleUpdates.FirstOrDefault(x => x.Path == _titleUpdateWindowData.Selected);
|
||||
List<TitleUpdateModel> enabled = TitleUpdates.Where(x => x.IsEnabled).ToList();
|
||||
TitleUpdateModel selected = _titleUpdates.FirstOrDefault(x => x.Path == _titleUpdateWindowData.Selected);
|
||||
List<TitleUpdateModel> enabled = _titleUpdates.Where(x => x.IsEnabled).ToList();
|
||||
|
||||
foreach (TitleUpdateModel update in enabled)
|
||||
{
|
||||
@ -111,43 +117,47 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
|
||||
private void AddUpdate(string path)
|
||||
{
|
||||
if (File.Exists(path) && !TitleUpdates.Any(x => x.Path == path))
|
||||
if (File.Exists(path) && !_titleUpdates.Any(x => x.Path == path))
|
||||
{
|
||||
using (FileStream file = new(path, FileMode.Open, FileAccess.Read))
|
||||
using FileStream file = new(path, FileMode.Open, FileAccess.Read);
|
||||
|
||||
try
|
||||
{
|
||||
PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage());
|
||||
(Nca patchNca, Nca controlNca) = ApplicationLoader.GetGameUpdateDataFromPartition(_virtualFileSystem, new PartitionFileSystem(file.AsStorage()), _titleId.ToString("x16"), 0);
|
||||
|
||||
try
|
||||
if (controlNca != null && patchNca != null)
|
||||
{
|
||||
(Nca patchNca, Nca controlNca) = ApplicationLoader.GetGameUpdateDataFromPartition(VirtualFileSystem, nsp, TitleId, 0);
|
||||
ApplicationControlProperty controlData = new();
|
||||
|
||||
if (controlNca != null && patchNca != null)
|
||||
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)
|
||||
{
|
||||
ApplicationControlProperty controlData = new ApplicationControlProperty();
|
||||
|
||||
using var nacpFile = new UniqueRef<IFile>();
|
||||
|
||||
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["DialogUpdateAddUpdateErrorMessage"]);
|
||||
});
|
||||
update.IsEnabled = false;
|
||||
}
|
||||
|
||||
_titleUpdates.Last().IsEnabled = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
else
|
||||
{
|
||||
Dispatcher.UIThread.Post(async () =>
|
||||
{
|
||||
await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance["DialogDlcLoadNcaErrorMessage"], ex.Message, path));
|
||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance["DialogUpdateAddUpdateErrorMessage"]);
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Dispatcher.UIThread.Post(async () =>
|
||||
{
|
||||
await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance["DialogDlcLoadNcaErrorMessage"], ex.Message, path));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -155,16 +165,17 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
{
|
||||
if (removeSelectedOnly)
|
||||
{
|
||||
TitleUpdates.RemoveAll(TitleUpdates.Where(x => x.IsEnabled && !x.IsNoUpdate).ToList());
|
||||
_titleUpdates.RemoveAll(_titleUpdates.Where(x => x.IsEnabled && !x.IsNoUpdate).ToList());
|
||||
}
|
||||
else
|
||||
{
|
||||
TitleUpdates.RemoveAll(TitleUpdates.Where(x => !x.IsNoUpdate).ToList());
|
||||
_titleUpdates.RemoveAll(_titleUpdates.Where(x => !x.IsNoUpdate).ToList());
|
||||
}
|
||||
|
||||
TitleUpdates.FirstOrDefault(x => x.IsNoUpdate).IsEnabled = true;
|
||||
_titleUpdates.FirstOrDefault(x => x.IsNoUpdate).IsEnabled = true;
|
||||
|
||||
SortUpdates();
|
||||
PrintHeading();
|
||||
}
|
||||
|
||||
public void RemoveSelected()
|
||||
@ -179,7 +190,7 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
|
||||
public async void Add()
|
||||
{
|
||||
OpenFileDialog dialog = new OpenFileDialog()
|
||||
OpenFileDialog dialog = new()
|
||||
{
|
||||
Title = LocaleManager.Instance["SelectUpdateDialogTitle"],
|
||||
AllowMultiple = true
|
||||
@ -202,11 +213,12 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
}
|
||||
|
||||
SortUpdates();
|
||||
PrintHeading();
|
||||
}
|
||||
|
||||
private void SortUpdates()
|
||||
{
|
||||
var list = TitleUpdates.ToList();
|
||||
var list = _titleUpdates.ToList();
|
||||
|
||||
list.Sort((first, second) =>
|
||||
{
|
||||
@ -222,8 +234,8 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
return Version.Parse(first.Control.DisplayVersionString.ToString()).CompareTo(Version.Parse(second.Control.DisplayVersionString.ToString())) * -1;
|
||||
});
|
||||
|
||||
TitleUpdates.Clear();
|
||||
TitleUpdates.AddRange(list);
|
||||
_titleUpdates.Clear();
|
||||
_titleUpdates.AddRange(list);
|
||||
}
|
||||
|
||||
public void Save()
|
||||
@ -232,7 +244,7 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
|
||||
_titleUpdateWindowData.Selected = "";
|
||||
|
||||
foreach (TitleUpdateModel update in TitleUpdates)
|
||||
foreach (TitleUpdateModel update in _titleUpdates)
|
||||
{
|
||||
_titleUpdateWindowData.Paths.Add(update.Path);
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.Common
|
||||
@ -7,49 +8,15 @@ namespace Ryujinx.Common
|
||||
public static class BinaryReaderExtensions
|
||||
{
|
||||
public unsafe static T ReadStruct<T>(this BinaryReader reader)
|
||||
where T : struct
|
||||
where T : unmanaged
|
||||
{
|
||||
int size = Marshal.SizeOf<T>();
|
||||
|
||||
byte[] data = reader.ReadBytes(size);
|
||||
|
||||
fixed (byte* ptr = data)
|
||||
{
|
||||
return Marshal.PtrToStructure<T>((IntPtr)ptr);
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe static T[] ReadStructArray<T>(this BinaryReader reader, int count)
|
||||
where T : struct
|
||||
{
|
||||
int size = Marshal.SizeOf<T>();
|
||||
|
||||
T[] result = new T[count];
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
byte[] data = reader.ReadBytes(size);
|
||||
|
||||
fixed (byte* ptr = data)
|
||||
{
|
||||
result[i] = Marshal.PtrToStructure<T>((IntPtr)ptr);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return MemoryMarshal.Cast<byte, T>(reader.ReadBytes(Unsafe.SizeOf<T>()))[0];
|
||||
}
|
||||
|
||||
public unsafe static void WriteStruct<T>(this BinaryWriter writer, T value)
|
||||
where T : struct
|
||||
where T : unmanaged
|
||||
{
|
||||
long size = Marshal.SizeOf<T>();
|
||||
|
||||
byte[] data = new byte[size];
|
||||
|
||||
fixed (byte* ptr = data)
|
||||
{
|
||||
Marshal.StructureToPtr<T>(value, (IntPtr)ptr, false);
|
||||
}
|
||||
ReadOnlySpan<byte> data = MemoryMarshal.Cast<T, byte>(MemoryMarshal.CreateReadOnlySpan(ref value, 1));
|
||||
|
||||
writer.Write(data);
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MsgPack.Cli" Version="1.0.1" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
|
||||
<PackageReference Include="System.Management" Version="6.0.0" />
|
||||
<PackageReference Include="System.Management" Version="7.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -180,6 +180,37 @@ namespace Ryujinx.Cpu.Jit
|
||||
WriteImpl(va, data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan<byte> data)
|
||||
{
|
||||
if (data.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
SignalMemoryTracking(va, (ulong)data.Length, false);
|
||||
|
||||
if (IsContiguousAndMapped(va, data.Length))
|
||||
{
|
||||
var target = _backingMemory.GetSpan(GetPhysicalAddressInternal(va), data.Length);
|
||||
|
||||
bool changed = !data.SequenceEqual(target);
|
||||
|
||||
if (changed)
|
||||
{
|
||||
data.CopyTo(target);
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteImpl(va, data);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes data to CPU mapped memory.
|
||||
/// </summary>
|
||||
|
@ -307,6 +307,34 @@ namespace Ryujinx.Cpu.Jit
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan<byte> data)
|
||||
{
|
||||
try
|
||||
{
|
||||
SignalMemoryTracking(va, (ulong)data.Length, false);
|
||||
|
||||
Span<byte> target = _addressSpaceMirror.GetSpan(va, data.Length);
|
||||
bool changed = !data.SequenceEqual(target);
|
||||
|
||||
if (changed)
|
||||
{
|
||||
data.CopyTo(target);
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
catch (InvalidMemoryRegionException)
|
||||
{
|
||||
if (_invalidAccessHandler == null || !_invalidAccessHandler(va))
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ReadOnlySpan<byte> GetSpan(ulong va, int size, bool tracked = false)
|
||||
{
|
||||
|
@ -1,6 +1,7 @@
|
||||
using Ryujinx.Memory;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
@ -23,34 +24,18 @@ namespace Ryujinx.Cpu
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe static T Read<T>(IVirtualMemoryManager memory, ulong position) where T : struct
|
||||
public unsafe static T Read<T>(IVirtualMemoryManager memory, ulong position) where T : unmanaged
|
||||
{
|
||||
long size = Marshal.SizeOf<T>();
|
||||
|
||||
byte[] data = new byte[size];
|
||||
|
||||
memory.Read(position, data);
|
||||
|
||||
fixed (byte* ptr = data)
|
||||
{
|
||||
return Marshal.PtrToStructure<T>((IntPtr)ptr);
|
||||
}
|
||||
return MemoryMarshal.Cast<byte, T>(memory.GetSpan(position, Unsafe.SizeOf<T>()))[0];
|
||||
}
|
||||
|
||||
public unsafe static ulong Write<T>(IVirtualMemoryManager memory, ulong position, T value) where T : struct
|
||||
public unsafe static ulong Write<T>(IVirtualMemoryManager memory, ulong position, T value) where T : unmanaged
|
||||
{
|
||||
long size = Marshal.SizeOf<T>();
|
||||
|
||||
byte[] data = new byte[size];
|
||||
|
||||
fixed (byte* ptr = data)
|
||||
{
|
||||
Marshal.StructureToPtr<T>(value, (IntPtr)ptr, false);
|
||||
}
|
||||
ReadOnlySpan<byte> data = MemoryMarshal.Cast<T, byte>(MemoryMarshal.CreateReadOnlySpan(ref value, 1));
|
||||
|
||||
memory.Write(position, data);
|
||||
|
||||
return (ulong)size;
|
||||
return (ulong)data.Length;
|
||||
}
|
||||
|
||||
public static string ReadAsciiString(IVirtualMemoryManager memory, ulong position, long maxSize = -1)
|
||||
|
14
Ryujinx.Graphics.GAL/BufferAssignment.cs
Normal file
14
Ryujinx.Graphics.GAL/BufferAssignment.cs
Normal file
@ -0,0 +1,14 @@
|
||||
namespace Ryujinx.Graphics.GAL
|
||||
{
|
||||
public struct BufferAssignment
|
||||
{
|
||||
public readonly int Binding;
|
||||
public readonly BufferRange Range;
|
||||
|
||||
public BufferAssignment(int binding, BufferRange range)
|
||||
{
|
||||
Binding = binding;
|
||||
Range = range;
|
||||
}
|
||||
}
|
||||
}
|
@ -86,12 +86,12 @@ namespace Ryujinx.Graphics.GAL
|
||||
|
||||
void SetStencilTest(StencilTestDescriptor stencilTest);
|
||||
|
||||
void SetStorageBuffers(int first, ReadOnlySpan<BufferRange> buffers);
|
||||
void SetStorageBuffers(ReadOnlySpan<BufferAssignment> buffers);
|
||||
|
||||
void SetTextureAndSampler(ShaderStage stage, int binding, ITexture texture, ISampler sampler);
|
||||
|
||||
void SetTransformFeedbackBuffers(ReadOnlySpan<BufferRange> buffers);
|
||||
void SetUniformBuffers(int first, ReadOnlySpan<BufferRange> buffers);
|
||||
void SetUniformBuffers(ReadOnlySpan<BufferAssignment> buffers);
|
||||
|
||||
void SetUserClipDistance(int index, bool enableClip);
|
||||
|
||||
|
@ -29,6 +29,7 @@ namespace Ryujinx.Graphics.GAL
|
||||
ReadOnlySpan<byte> GetBufferData(BufferHandle buffer, int offset, int size);
|
||||
|
||||
Capabilities GetCapabilities();
|
||||
ulong GetCurrentSync();
|
||||
HardwareInfo GetHardwareInfo();
|
||||
|
||||
IProgram LoadProgramBinary(byte[] programBinary, bool hasFragmentShader, ShaderInfo info);
|
||||
|
@ -142,6 +142,30 @@ namespace Ryujinx.Graphics.GAL.Multithreading
|
||||
return ranges;
|
||||
}
|
||||
|
||||
internal Span<BufferAssignment> MapBufferRanges(Span<BufferAssignment> ranges)
|
||||
{
|
||||
// Rewrite the buffer ranges to point to the mapped handles.
|
||||
|
||||
lock (_bufferMap)
|
||||
{
|
||||
for (int i = 0; i < ranges.Length; i++)
|
||||
{
|
||||
ref BufferAssignment assignment = ref ranges[i];
|
||||
BufferRange range = assignment.Range;
|
||||
BufferHandle result;
|
||||
|
||||
if (!_bufferMap.TryGetValue(range.Handle, out result))
|
||||
{
|
||||
result = BufferHandle.Null;
|
||||
}
|
||||
|
||||
assignment = new BufferAssignment(ranges[i].Binding, new BufferRange(result, range.Offset, range.Size));
|
||||
}
|
||||
}
|
||||
|
||||
return ranges;
|
||||
}
|
||||
|
||||
internal Span<VertexBufferDescriptor> MapBufferRanges(Span<VertexBufferDescriptor> ranges)
|
||||
{
|
||||
// Rewrite the buffer ranges to point to the mapped handles.
|
||||
|
@ -6,19 +6,17 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Commands
|
||||
struct SetStorageBuffersCommand : IGALCommand
|
||||
{
|
||||
public CommandType CommandType => CommandType.SetStorageBuffers;
|
||||
private int _first;
|
||||
private SpanRef<BufferRange> _buffers;
|
||||
private SpanRef<BufferAssignment> _buffers;
|
||||
|
||||
public void Set(int first, SpanRef<BufferRange> buffers)
|
||||
public void Set(SpanRef<BufferAssignment> buffers)
|
||||
{
|
||||
_first = first;
|
||||
_buffers = buffers;
|
||||
}
|
||||
|
||||
public static void Run(ref SetStorageBuffersCommand command, ThreadedRenderer threaded, IRenderer renderer)
|
||||
{
|
||||
Span<BufferRange> buffers = command._buffers.Get(threaded);
|
||||
renderer.Pipeline.SetStorageBuffers(command._first, threaded.Buffers.MapBufferRanges(buffers));
|
||||
Span<BufferAssignment> buffers = command._buffers.Get(threaded);
|
||||
renderer.Pipeline.SetStorageBuffers(threaded.Buffers.MapBufferRanges(buffers));
|
||||
command._buffers.Dispose(threaded);
|
||||
}
|
||||
}
|
||||
|
@ -6,19 +6,17 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Commands
|
||||
struct SetUniformBuffersCommand : IGALCommand
|
||||
{
|
||||
public CommandType CommandType => CommandType.SetUniformBuffers;
|
||||
private int _first;
|
||||
private SpanRef<BufferRange> _buffers;
|
||||
private SpanRef<BufferAssignment> _buffers;
|
||||
|
||||
public void Set(int first, SpanRef<BufferRange> buffers)
|
||||
public void Set(SpanRef<BufferAssignment> buffers)
|
||||
{
|
||||
_first = first;
|
||||
_buffers = buffers;
|
||||
}
|
||||
|
||||
public static void Run(ref SetUniformBuffersCommand command, ThreadedRenderer threaded, IRenderer renderer)
|
||||
{
|
||||
Span<BufferRange> buffers = command._buffers.Get(threaded);
|
||||
renderer.Pipeline.SetUniformBuffers(command._first, threaded.Buffers.MapBufferRanges(buffers));
|
||||
Span<BufferAssignment> buffers = command._buffers.Get(threaded);
|
||||
renderer.Pipeline.SetUniformBuffers(threaded.Buffers.MapBufferRanges(buffers));
|
||||
command._buffers.Dispose(threaded);
|
||||
}
|
||||
}
|
||||
|
@ -275,9 +275,9 @@ namespace Ryujinx.Graphics.GAL.Multithreading
|
||||
_renderer.QueueCommand();
|
||||
}
|
||||
|
||||
public void SetStorageBuffers(int first, ReadOnlySpan<BufferRange> buffers)
|
||||
public void SetStorageBuffers(ReadOnlySpan<BufferAssignment> buffers)
|
||||
{
|
||||
_renderer.New<SetStorageBuffersCommand>().Set(first, _renderer.CopySpan(buffers));
|
||||
_renderer.New<SetStorageBuffersCommand>().Set(_renderer.CopySpan(buffers));
|
||||
_renderer.QueueCommand();
|
||||
}
|
||||
|
||||
@ -293,9 +293,9 @@ namespace Ryujinx.Graphics.GAL.Multithreading
|
||||
_renderer.QueueCommand();
|
||||
}
|
||||
|
||||
public void SetUniformBuffers(int first, ReadOnlySpan<BufferRange> buffers)
|
||||
public void SetUniformBuffers(ReadOnlySpan<BufferAssignment> buffers)
|
||||
{
|
||||
_renderer.New<SetUniformBuffersCommand>().Set(first, _renderer.CopySpan(buffers));
|
||||
_renderer.New<SetUniformBuffersCommand>().Set(_renderer.CopySpan(buffers));
|
||||
_renderer.QueueCommand();
|
||||
}
|
||||
|
||||
|
@ -338,6 +338,11 @@ namespace Ryujinx.Graphics.GAL.Multithreading
|
||||
return box.Result;
|
||||
}
|
||||
|
||||
public ulong GetCurrentSync()
|
||||
{
|
||||
return _baseRenderer.GetCurrentSync();
|
||||
}
|
||||
|
||||
public HardwareInfo GetHardwareInfo()
|
||||
{
|
||||
return _baseRenderer.GetHardwareInfo();
|
||||
@ -422,7 +427,11 @@ namespace Ryujinx.Graphics.GAL.Multithreading
|
||||
|
||||
// Stop the GPU thread.
|
||||
_disposed = true;
|
||||
_gpuThread.Join();
|
||||
|
||||
if (_gpuThread != null && _gpuThread.IsAlive)
|
||||
{
|
||||
_gpuThread.Join();
|
||||
}
|
||||
|
||||
// Dispose the renderer.
|
||||
_baseRenderer.Dispose();
|
||||
@ -435,4 +444,4 @@ namespace Ryujinx.Graphics.GAL.Multithreading
|
||||
Sync.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -51,16 +51,35 @@ namespace Ryujinx.Graphics.Gpu.Engine.GPFifo
|
||||
/// </summary>
|
||||
public uint EntryCount;
|
||||
|
||||
/// <summary>
|
||||
/// Get the entries for the command buffer from memory.
|
||||
/// </summary>
|
||||
/// <param name="memoryManager">The memory manager used to fetch the data</param>
|
||||
/// <param name="flush">If true, flushes potential GPU written data before reading the command buffer</param>
|
||||
/// <returns>The fetched data</returns>
|
||||
private ReadOnlySpan<int> GetWords(MemoryManager memoryManager, bool flush)
|
||||
{
|
||||
return MemoryMarshal.Cast<byte, int>(memoryManager.GetSpan(EntryAddress, (int)EntryCount * 4, flush));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prefetch the command buffer.
|
||||
/// </summary>
|
||||
/// <param name="memoryManager">The memory manager used to fetch the data</param>
|
||||
public void Prefetch(MemoryManager memoryManager)
|
||||
{
|
||||
Words = GetWords(memoryManager, true).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetch the command buffer.
|
||||
/// </summary>
|
||||
/// <param name="memoryManager">The memory manager used to fetch the data</param>
|
||||
/// <param name="flush">If true, flushes potential GPU written data before reading the command buffer</param>
|
||||
public void Fetch(MemoryManager memoryManager, bool flush = true)
|
||||
/// <returns>The command buffer words</returns>
|
||||
public ReadOnlySpan<int> Fetch(MemoryManager memoryManager, bool flush)
|
||||
{
|
||||
if (Words == null)
|
||||
{
|
||||
Words = MemoryMarshal.Cast<byte, int>(memoryManager.GetSpan(EntryAddress, (int)EntryCount * 4, flush)).ToArray();
|
||||
}
|
||||
return Words ?? GetWords(memoryManager, flush);
|
||||
}
|
||||
}
|
||||
|
||||
@ -158,7 +177,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.GPFifo
|
||||
|
||||
if (beforeBarrier && commandBuffer.Type == CommandBufferType.Prefetch)
|
||||
{
|
||||
commandBuffer.Fetch(processor.MemoryManager);
|
||||
commandBuffer.Prefetch(processor.MemoryManager);
|
||||
}
|
||||
|
||||
if (commandBuffer.Type == CommandBufferType.NoPrefetch)
|
||||
@ -199,7 +218,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.GPFifo
|
||||
}
|
||||
|
||||
_currentCommandBuffer = entry;
|
||||
_currentCommandBuffer.Fetch(entry.Processor.MemoryManager, flushCommandBuffer);
|
||||
ReadOnlySpan<int> words = entry.Fetch(entry.Processor.MemoryManager, flushCommandBuffer);
|
||||
|
||||
// If we are changing the current channel,
|
||||
// we need to force all the host state to be updated.
|
||||
@ -209,7 +228,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.GPFifo
|
||||
entry.Processor.ForceAllDirty();
|
||||
}
|
||||
|
||||
entry.Processor.Process(entry.EntryAddress, _currentCommandBuffer.Words);
|
||||
entry.Processor.Process(entry.EntryAddress, words);
|
||||
}
|
||||
|
||||
_interrupt = false;
|
||||
|
@ -8,6 +8,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
/// </summary>
|
||||
class ConstantBufferUpdater
|
||||
{
|
||||
private const int UniformDataCacheSize = 512;
|
||||
|
||||
private readonly GpuChannel _channel;
|
||||
private readonly DeviceStateWithShadow<ThreedClassState> _state;
|
||||
|
||||
@ -16,6 +18,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
private ulong _ubBeginCpuAddress = 0;
|
||||
private ulong _ubFollowUpAddress = 0;
|
||||
private ulong _ubByteCount = 0;
|
||||
private int _ubIndex = 0;
|
||||
private int[] _ubData = new int[UniformDataCacheSize];
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the constant buffer updater.
|
||||
@ -108,9 +112,16 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
if (_ubFollowUpAddress != 0)
|
||||
{
|
||||
var memoryManager = _channel.MemoryManager;
|
||||
memoryManager.Physical.BufferCache.ForceDirty(memoryManager, _ubFollowUpAddress - _ubByteCount, _ubByteCount);
|
||||
|
||||
Span<byte> data = MemoryMarshal.Cast<int, byte>(_ubData.AsSpan(0, (int)(_ubByteCount / 4)));
|
||||
|
||||
if (memoryManager.Physical.WriteWithRedundancyCheck(_ubBeginCpuAddress, data))
|
||||
{
|
||||
memoryManager.Physical.BufferCache.ForceDirty(memoryManager, _ubFollowUpAddress - _ubByteCount, _ubByteCount);
|
||||
}
|
||||
|
||||
_ubFollowUpAddress = 0;
|
||||
_ubIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,7 +135,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
|
||||
ulong address = uniformBuffer.Address.Pack() + (uint)uniformBuffer.Offset;
|
||||
|
||||
if (_ubFollowUpAddress != address)
|
||||
if (_ubFollowUpAddress != address || _ubIndex == _ubData.Length)
|
||||
{
|
||||
FlushUboDirty();
|
||||
|
||||
@ -132,8 +143,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
_ubBeginCpuAddress = _channel.MemoryManager.Translate(address);
|
||||
}
|
||||
|
||||
var byteData = MemoryMarshal.Cast<int, byte>(MemoryMarshal.CreateSpan(ref argument, 1));
|
||||
_channel.MemoryManager.Physical.WriteUntracked(_ubBeginCpuAddress + _ubByteCount, byteData);
|
||||
_ubData[_ubIndex++] = argument;
|
||||
|
||||
_ubFollowUpAddress = address + 4;
|
||||
_ubByteCount += 4;
|
||||
@ -153,7 +163,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
|
||||
ulong size = (ulong)data.Length * 4;
|
||||
|
||||
if (_ubFollowUpAddress != address)
|
||||
if (_ubFollowUpAddress != address || _ubIndex + data.Length > _ubData.Length)
|
||||
{
|
||||
FlushUboDirty();
|
||||
|
||||
@ -161,8 +171,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
_ubBeginCpuAddress = _channel.MemoryManager.Translate(address);
|
||||
}
|
||||
|
||||
var byteData = MemoryMarshal.Cast<int, byte>(data);
|
||||
_channel.MemoryManager.Physical.WriteUntracked(_ubBeginCpuAddress + _ubByteCount, byteData);
|
||||
data.CopyTo(_ubData.AsSpan(_ubIndex));
|
||||
_ubIndex += data.Length;
|
||||
|
||||
_ubFollowUpAddress = address + size;
|
||||
_ubByteCount += size;
|
||||
|
@ -372,7 +372,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
float srcX1 = ((float)_state.State.DrawTextureDuDx / (1UL << 32)) * dstWidth + srcX0;
|
||||
float srcY1 = ((float)_state.State.DrawTextureDvDy / (1UL << 32)) * dstHeight + srcY0;
|
||||
|
||||
engine.UpdateState();
|
||||
engine.UpdateState(ulong.MaxValue & ~(1UL << StateUpdater.ShaderStateIndex));
|
||||
|
||||
_channel.TextureManager.UpdateRenderTargets();
|
||||
|
||||
int textureId = _state.State.DrawTextureTextureId;
|
||||
int samplerId = _state.State.DrawTextureSamplerId;
|
||||
|
@ -37,6 +37,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
private bool _vsUsesDrawParameters;
|
||||
private bool _vtgWritesRtLayer;
|
||||
private byte _vsClipDistancesWritten;
|
||||
private uint _vbEnableMask;
|
||||
|
||||
private bool _prevDrawIndexed;
|
||||
private bool _prevDrawIndirect;
|
||||
@ -76,6 +77,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
nameof(ThreedClassState.VertexBufferState),
|
||||
nameof(ThreedClassState.VertexBufferEndAddress)),
|
||||
|
||||
// Must be done after vertex buffer updates.
|
||||
new StateUpdateCallbackEntry(UpdateVertexAttribState, nameof(ThreedClassState.VertexAttribState)),
|
||||
|
||||
new StateUpdateCallbackEntry(UpdateBlendState,
|
||||
@ -852,12 +854,23 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
/// </summary>
|
||||
private void UpdateVertexAttribState()
|
||||
{
|
||||
uint vbEnableMask = _vbEnableMask;
|
||||
|
||||
Span<VertexAttribDescriptor> vertexAttribs = stackalloc VertexAttribDescriptor[Constants.TotalVertexAttribs];
|
||||
|
||||
for (int index = 0; index < Constants.TotalVertexAttribs; index++)
|
||||
{
|
||||
var vertexAttrib = _state.State.VertexAttribState[index];
|
||||
|
||||
int bufferIndex = vertexAttrib.UnpackBufferIndex();
|
||||
|
||||
if ((vbEnableMask & (1u << bufferIndex)) == 0)
|
||||
{
|
||||
// Using a vertex buffer that doesn't exist is invalid, so let's use a dummy attribute for those cases.
|
||||
vertexAttribs[index] = new VertexAttribDescriptor(0, 0, true, Format.R32G32B32A32Float);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!FormatTable.TryGetAttribFormat(vertexAttrib.UnpackFormat(), out Format format))
|
||||
{
|
||||
Logger.Debug?.Print(LogClass.Gpu, $"Invalid attribute format 0x{vertexAttrib.UnpackFormat():X}.");
|
||||
@ -866,7 +879,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
}
|
||||
|
||||
vertexAttribs[index] = new VertexAttribDescriptor(
|
||||
vertexAttrib.UnpackBufferIndex(),
|
||||
bufferIndex,
|
||||
vertexAttrib.UnpackOffset(),
|
||||
vertexAttrib.UnpackIsConstant(),
|
||||
format);
|
||||
@ -954,6 +967,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
|
||||
bool drawIndexed = _drawState.DrawIndexed;
|
||||
bool drawIndirect = _drawState.DrawIndirect;
|
||||
uint vbEnableMask = 0;
|
||||
|
||||
for (int index = 0; index < Constants.TotalVertexBuffers; index++)
|
||||
{
|
||||
@ -971,6 +985,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
|
||||
ulong address = vertexBuffer.Address.Pack();
|
||||
|
||||
if (_channel.MemoryManager.IsMapped(address))
|
||||
{
|
||||
vbEnableMask |= 1u << index;
|
||||
}
|
||||
|
||||
int stride = vertexBuffer.UnpackStride();
|
||||
|
||||
bool instanced = _state.State.VertexBufferInstanced[index];
|
||||
@ -1017,6 +1036,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
_pipeline.VertexBuffers[index] = new BufferPipelineDescriptor(_channel.MemoryManager.IsMapped(address), stride, divisor);
|
||||
_channel.BufferManager.SetVertexBuffer(index, address, size, stride, divisor);
|
||||
}
|
||||
|
||||
if (_vbEnableMask != vbEnableMask)
|
||||
{
|
||||
_vbEnableMask = vbEnableMask;
|
||||
UpdateVertexAttribState();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -69,6 +69,12 @@ namespace Ryujinx.Graphics.Gpu
|
||||
/// </summary>
|
||||
internal List<Action> SyncpointActions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Buffer migrations that are currently in-flight. These are checked whenever sync is created to determine if buffer migration
|
||||
/// copies have completed on the GPU, and their data can be freed.
|
||||
/// </summary>
|
||||
internal List<BufferMigration> BufferMigrations { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Queue with deferred actions that must run on the render thread.
|
||||
/// </summary>
|
||||
@ -90,6 +96,7 @@ namespace Ryujinx.Graphics.Gpu
|
||||
public event Action<ShaderCacheState, int, int> ShaderCacheStateChanged;
|
||||
|
||||
private Thread _gpuThread;
|
||||
private bool _pendingSync;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the GPU emulation context.
|
||||
@ -109,6 +116,7 @@ namespace Ryujinx.Graphics.Gpu
|
||||
|
||||
SyncActions = new List<Action>();
|
||||
SyncpointActions = new List<Action>();
|
||||
BufferMigrations = new List<BufferMigration>();
|
||||
|
||||
DeferredActions = new Queue<Action>();
|
||||
|
||||
@ -273,6 +281,17 @@ namespace Ryujinx.Graphics.Gpu
|
||||
SequenceNumber++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a buffer migration. These are checked to see if they can be disposed when the sync number increases,
|
||||
/// and the migration copy has completed.
|
||||
/// </summary>
|
||||
/// <param name="migration">The buffer migration</param>
|
||||
internal void RegisterBufferMigration(BufferMigration migration)
|
||||
{
|
||||
BufferMigrations.Add(migration);
|
||||
_pendingSync = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers an action to be performed the next time a syncpoint is incremented.
|
||||
/// This will also ensure a host sync object is created, and <see cref="SyncNumber"/> is incremented.
|
||||
@ -288,6 +307,7 @@ namespace Ryujinx.Graphics.Gpu
|
||||
else
|
||||
{
|
||||
SyncActions.Add(action);
|
||||
_pendingSync = true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -298,7 +318,24 @@ namespace Ryujinx.Graphics.Gpu
|
||||
/// <param name="syncpoint">True if host sync is being created by a syncpoint</param>
|
||||
public void CreateHostSyncIfNeeded(bool syncpoint)
|
||||
{
|
||||
if (SyncActions.Count > 0 || (syncpoint && SyncpointActions.Count > 0))
|
||||
if (BufferMigrations.Count > 0)
|
||||
{
|
||||
ulong currentSyncNumber = Renderer.GetCurrentSync();
|
||||
|
||||
for (int i = 0; i < BufferMigrations.Count; i++)
|
||||
{
|
||||
BufferMigration migration = BufferMigrations[i];
|
||||
long diff = (long)(currentSyncNumber - migration.SyncNumber);
|
||||
|
||||
if (diff >= 0)
|
||||
{
|
||||
migration.Dispose();
|
||||
BufferMigrations.RemoveAt(i--);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_pendingSync || (syncpoint && SyncpointActions.Count > 0))
|
||||
{
|
||||
Renderer.CreateSync(SyncNumber);
|
||||
|
||||
@ -317,6 +354,8 @@ namespace Ryujinx.Graphics.Gpu
|
||||
SyncActions.Clear();
|
||||
SyncpointActions.Clear();
|
||||
}
|
||||
|
||||
_pendingSync = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -65,6 +65,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
private bool _useGranular;
|
||||
private bool _syncActionRegistered;
|
||||
|
||||
private int _referenceCount = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the buffer.
|
||||
/// </summary>
|
||||
@ -229,7 +231,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
{
|
||||
if (_modifiedRanges == null)
|
||||
{
|
||||
_modifiedRanges = new BufferModifiedRangeList(_context);
|
||||
_modifiedRanges = new BufferModifiedRangeList(_context, this, Flush);
|
||||
}
|
||||
}
|
||||
|
||||
@ -290,7 +292,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
/// <param name="from">The buffer to inherit from</param>
|
||||
public void InheritModifiedRanges(Buffer from)
|
||||
{
|
||||
if (from._modifiedRanges != null)
|
||||
if (from._modifiedRanges != null && from._modifiedRanges.HasRanges)
|
||||
{
|
||||
if (from._syncActionRegistered && !_syncActionRegistered)
|
||||
{
|
||||
@ -310,17 +312,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
}
|
||||
};
|
||||
|
||||
if (_modifiedRanges == null)
|
||||
{
|
||||
_modifiedRanges = from._modifiedRanges;
|
||||
_modifiedRanges.ReregisterRanges(registerRangeAction);
|
||||
EnsureRangeList();
|
||||
|
||||
from._modifiedRanges = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
_modifiedRanges.InheritRanges(from._modifiedRanges, registerRangeAction);
|
||||
}
|
||||
_modifiedRanges.InheritRanges(from._modifiedRanges, registerRangeAction);
|
||||
}
|
||||
}
|
||||
|
||||
@ -456,7 +450,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
if (ranges != null)
|
||||
{
|
||||
(address, size) = PageAlign(address, size);
|
||||
ranges.WaitForAndGetRanges(address, size, Flush);
|
||||
ranges.WaitForAndFlushRanges(address, size);
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
@ -508,6 +502,25 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
UnmappedSequence++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Increments the buffer reference count.
|
||||
/// </summary>
|
||||
public void IncrementReferenceCount()
|
||||
{
|
||||
_referenceCount++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decrements the buffer reference count.
|
||||
/// </summary>
|
||||
public void DecrementReferenceCount()
|
||||
{
|
||||
if (--_referenceCount == 0)
|
||||
{
|
||||
DisposeData();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the host buffer's data, not its tracking handles.
|
||||
/// </summary>
|
||||
@ -528,7 +541,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
_memoryTrackingGranular?.Dispose();
|
||||
_memoryTracking?.Dispose();
|
||||
|
||||
DisposeData();
|
||||
DecrementReferenceCount();
|
||||
}
|
||||
}
|
||||
}
|
@ -22,6 +22,10 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
private readonly GpuContext _context;
|
||||
private readonly PhysicalMemory _physicalMemory;
|
||||
|
||||
/// <remarks>
|
||||
/// Only modified from the GPU thread. Must lock for add/remove.
|
||||
/// Must lock for any access from other threads.
|
||||
/// </remarks>
|
||||
private readonly RangeList<Buffer> _buffers;
|
||||
|
||||
private Buffer[] _bufferOverlaps;
|
||||
@ -200,12 +204,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
/// <param name="size">Size in bytes of the buffer</param>
|
||||
private void CreateBufferAligned(ulong address, ulong size)
|
||||
{
|
||||
int overlapsCount;
|
||||
|
||||
lock (_buffers)
|
||||
{
|
||||
overlapsCount = _buffers.FindOverlapsNonOverlapping(address, size, ref _bufferOverlaps);
|
||||
}
|
||||
int overlapsCount = _buffers.FindOverlapsNonOverlapping(address, size, ref _bufferOverlaps);
|
||||
|
||||
if (overlapsCount != 0)
|
||||
{
|
||||
@ -274,7 +273,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
buffer.CopyTo(newBuffer, dstOffset);
|
||||
newBuffer.InheritModifiedRanges(buffer);
|
||||
|
||||
buffer.DisposeData();
|
||||
buffer.DecrementReferenceCount();
|
||||
}
|
||||
|
||||
newBuffer.SynchronizeMemory(address, newSize);
|
||||
@ -410,10 +409,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
|
||||
if (size != 0)
|
||||
{
|
||||
lock (_buffers)
|
||||
{
|
||||
buffer = _buffers.FindFirstOverlap(address, size);
|
||||
}
|
||||
buffer = _buffers.FindFirstOverlap(address, size);
|
||||
|
||||
buffer.SynchronizeMemory(address, size);
|
||||
|
||||
@ -424,10 +420,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
}
|
||||
else
|
||||
{
|
||||
lock (_buffers)
|
||||
{
|
||||
buffer = _buffers.FindFirstOverlap(address, 1);
|
||||
}
|
||||
buffer = _buffers.FindFirstOverlap(address, 1);
|
||||
}
|
||||
|
||||
return buffer;
|
||||
@ -442,12 +435,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
{
|
||||
if (size != 0)
|
||||
{
|
||||
Buffer buffer;
|
||||
|
||||
lock (_buffers)
|
||||
{
|
||||
buffer = _buffers.FindFirstOverlap(address, size);
|
||||
}
|
||||
Buffer buffer = _buffers.FindFirstOverlap(address, size);
|
||||
|
||||
buffer.SynchronizeMemory(address, size);
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
private readonly VertexBuffer[] _vertexBuffers;
|
||||
private readonly BufferBounds[] _transformFeedbackBuffers;
|
||||
private readonly List<BufferTextureBinding> _bufferTextures;
|
||||
private readonly BufferRange[] _ranges;
|
||||
private readonly BufferAssignment[] _ranges;
|
||||
|
||||
/// <summary>
|
||||
/// Holds shader stage buffer state and binding information.
|
||||
@ -134,7 +134,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
|
||||
_bufferTextures = new List<BufferTextureBinding>();
|
||||
|
||||
_ranges = new BufferRange[Constants.TotalGpUniformBuffers * Constants.ShaderStages];
|
||||
_ranges = new BufferAssignment[Constants.TotalGpUniformBuffers * Constants.ShaderStages];
|
||||
}
|
||||
|
||||
|
||||
@ -618,10 +618,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void BindBuffers(BufferCache bufferCache, BuffersPerStage[] bindings, bool isStorage)
|
||||
{
|
||||
int rangesFirst = 0;
|
||||
int rangesCount = 0;
|
||||
|
||||
Span<BufferRange> ranges = _ranges;
|
||||
Span<BufferAssignment> ranges = _ranges;
|
||||
|
||||
for (ShaderStage stage = ShaderStage.Vertex; stage <= ShaderStage.Fragment; stage++)
|
||||
{
|
||||
@ -640,25 +639,14 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
? bufferCache.GetBufferRangeTillEnd(bounds.Address, bounds.Size, isWrite)
|
||||
: bufferCache.GetBufferRange(bounds.Address, bounds.Size);
|
||||
|
||||
if (rangesCount == 0)
|
||||
{
|
||||
rangesFirst = bindingInfo.Binding;
|
||||
}
|
||||
else if (bindingInfo.Binding != rangesFirst + rangesCount)
|
||||
{
|
||||
SetHostBuffers(ranges, rangesFirst, rangesCount, isStorage);
|
||||
rangesFirst = bindingInfo.Binding;
|
||||
rangesCount = 0;
|
||||
}
|
||||
|
||||
ranges[rangesCount++] = range;
|
||||
ranges[rangesCount++] = new BufferAssignment(bindingInfo.Binding, range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rangesCount != 0)
|
||||
{
|
||||
SetHostBuffers(ranges, rangesFirst, rangesCount, isStorage);
|
||||
SetHostBuffers(ranges, rangesCount, isStorage);
|
||||
}
|
||||
}
|
||||
|
||||
@ -671,10 +659,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void BindBuffers(BufferCache bufferCache, BuffersPerStage buffers, bool isStorage)
|
||||
{
|
||||
int rangesFirst = 0;
|
||||
int rangesCount = 0;
|
||||
|
||||
Span<BufferRange> ranges = _ranges;
|
||||
Span<BufferAssignment> ranges = _ranges;
|
||||
|
||||
for (int index = 0; index < buffers.Count; index++)
|
||||
{
|
||||
@ -689,24 +676,13 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
? bufferCache.GetBufferRangeTillEnd(bounds.Address, bounds.Size, isWrite)
|
||||
: bufferCache.GetBufferRange(bounds.Address, bounds.Size);
|
||||
|
||||
if (rangesCount == 0)
|
||||
{
|
||||
rangesFirst = bindingInfo.Binding;
|
||||
}
|
||||
else if (bindingInfo.Binding != rangesFirst + rangesCount)
|
||||
{
|
||||
SetHostBuffers(ranges, rangesFirst, rangesCount, isStorage);
|
||||
rangesFirst = bindingInfo.Binding;
|
||||
rangesCount = 0;
|
||||
}
|
||||
|
||||
ranges[rangesCount++] = range;
|
||||
ranges[rangesCount++] = new BufferAssignment(bindingInfo.Binding, range);
|
||||
}
|
||||
}
|
||||
|
||||
if (rangesCount != 0)
|
||||
{
|
||||
SetHostBuffers(ranges, rangesFirst, rangesCount, isStorage);
|
||||
SetHostBuffers(ranges, rangesCount, isStorage);
|
||||
}
|
||||
}
|
||||
|
||||
@ -718,15 +694,15 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
/// <param name="count">Number of bindings</param>
|
||||
/// <param name="isStorage">Indicates if the buffers are storage or uniform buffers</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void SetHostBuffers(ReadOnlySpan<BufferRange> ranges, int first, int count, bool isStorage)
|
||||
private void SetHostBuffers(ReadOnlySpan<BufferAssignment> ranges, int count, bool isStorage)
|
||||
{
|
||||
if (isStorage)
|
||||
{
|
||||
_context.Renderer.Pipeline.SetStorageBuffers(first, ranges.Slice(0, count));
|
||||
_context.Renderer.Pipeline.SetStorageBuffers(ranges.Slice(0, count));
|
||||
}
|
||||
else
|
||||
{
|
||||
_context.Renderer.Pipeline.SetUniformBuffers(first, ranges.Slice(0, count));
|
||||
_context.Renderer.Pipeline.SetUniformBuffers(ranges.Slice(0, count));
|
||||
}
|
||||
}
|
||||
|
||||
|
125
Ryujinx.Graphics.Gpu/Memory/BufferMigration.cs
Normal file
125
Ryujinx.Graphics.Gpu/Memory/BufferMigration.cs
Normal file
@ -0,0 +1,125 @@
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Memory
|
||||
{
|
||||
/// <summary>
|
||||
/// A record of when buffer data was copied from one buffer to another, along with the SyncNumber when the migration will be complete.
|
||||
/// Keeps the source buffer alive for data flushes until the migration is complete.
|
||||
/// </summary>
|
||||
internal class BufferMigration : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The offset for the migrated region.
|
||||
/// </summary>
|
||||
private readonly ulong _offset;
|
||||
|
||||
/// <summary>
|
||||
/// The size for the migrated region.
|
||||
/// </summary>
|
||||
private readonly ulong _size;
|
||||
|
||||
/// <summary>
|
||||
/// The buffer that was migrated from.
|
||||
/// </summary>
|
||||
private readonly Buffer _buffer;
|
||||
|
||||
/// <summary>
|
||||
/// The source range action, to be called on overlap with an unreached sync number.
|
||||
/// </summary>
|
||||
private readonly Action<ulong, ulong> _sourceRangeAction;
|
||||
|
||||
/// <summary>
|
||||
/// The source range list.
|
||||
/// </summary>
|
||||
private readonly BufferModifiedRangeList _source;
|
||||
|
||||
/// <summary>
|
||||
/// The destination range list. This range list must be updated when flushing the source.
|
||||
/// </summary>
|
||||
public readonly BufferModifiedRangeList Destination;
|
||||
|
||||
/// <summary>
|
||||
/// The sync number that needs to be reached for this migration to be removed. This is set to the pending sync number on creation.
|
||||
/// </summary>
|
||||
public readonly ulong SyncNumber;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a record for a buffer migration.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The source buffer for this migration</param>
|
||||
/// <param name="sourceRangeAction">The flush action for the source buffer</param>
|
||||
/// <param name="source">The modified range list for the source buffer</param>
|
||||
/// <param name="dest">The modified range list for the destination buffer</param>
|
||||
/// <param name="syncNumber">The sync number for when the migration is complete</param>
|
||||
public BufferMigration(
|
||||
Buffer buffer,
|
||||
Action<ulong, ulong> sourceRangeAction,
|
||||
BufferModifiedRangeList source,
|
||||
BufferModifiedRangeList dest,
|
||||
ulong syncNumber)
|
||||
{
|
||||
_offset = buffer.Address;
|
||||
_size = buffer.Size;
|
||||
_buffer = buffer;
|
||||
_sourceRangeAction = sourceRangeAction;
|
||||
_source = source;
|
||||
Destination = dest;
|
||||
SyncNumber = syncNumber;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine if the given range overlaps this migration, and has not been completed yet.
|
||||
/// </summary>
|
||||
/// <param name="offset">Start offset</param>
|
||||
/// <param name="size">Range size</param>
|
||||
/// <param name="syncNumber">The sync number that was waited on</param>
|
||||
/// <returns>True if overlapping and in progress, false otherwise</returns>
|
||||
public bool Overlaps(ulong offset, ulong size, ulong syncNumber)
|
||||
{
|
||||
ulong end = offset + size;
|
||||
ulong destEnd = _offset + _size;
|
||||
long syncDiff = (long)(syncNumber - SyncNumber); // syncNumber is less if the copy has not completed.
|
||||
|
||||
return !(end <= _offset || offset >= destEnd) && syncDiff < 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine if the given range matches this migration.
|
||||
/// </summary>
|
||||
/// <param name="offset">Start offset</param>
|
||||
/// <param name="size">Range size</param>
|
||||
/// <returns>True if the range exactly matches, false otherwise</returns>
|
||||
public bool FullyMatches(ulong offset, ulong size)
|
||||
{
|
||||
return _offset == offset && _size == size;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform the migration source's range action on the range provided, clamped to the bounds of the source buffer.
|
||||
/// </summary>
|
||||
/// <param name="offset">Start offset</param>
|
||||
/// <param name="size">Range size</param>
|
||||
/// <param name="syncNumber">Current sync number</param>
|
||||
/// <param name="parent">The modified range list that originally owned this range</param>
|
||||
public void RangeActionWithMigration(ulong offset, ulong size, ulong syncNumber, BufferModifiedRangeList parent)
|
||||
{
|
||||
ulong end = offset + size;
|
||||
end = Math.Min(_offset + _size, end);
|
||||
offset = Math.Max(_offset, offset);
|
||||
|
||||
size = end - offset;
|
||||
|
||||
_source.RangeActionWithMigration(offset, size, syncNumber, parent, _sourceRangeAction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes this reference to the range list, potentially allowing for the source buffer to be disposed.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Destination.RemoveMigration(this);
|
||||
|
||||
_buffer.DecrementReferenceCount();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
using Ryujinx.Common.Pools;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Pools;
|
||||
using Ryujinx.Memory.Range;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Memory
|
||||
@ -30,17 +32,24 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
/// </summary>
|
||||
public ulong SyncNumber { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The range list that originally owned this range.
|
||||
/// </summary>
|
||||
public BufferModifiedRangeList Parent { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of a modified range.
|
||||
/// </summary>
|
||||
/// <param name="address">Start address of the range</param>
|
||||
/// <param name="size">Size of the range in bytes</param>
|
||||
/// <param name="syncNumber">The GPU sync number at the time of creation</param>
|
||||
public BufferModifiedRange(ulong address, ulong size, ulong syncNumber)
|
||||
/// <param name="parent">The range list that owns this range</param>
|
||||
public BufferModifiedRange(ulong address, ulong size, ulong syncNumber, BufferModifiedRangeList parent)
|
||||
{
|
||||
Address = address;
|
||||
Size = size;
|
||||
SyncNumber = syncNumber;
|
||||
Parent = parent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -63,16 +72,39 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
private const int BackingInitialSize = 8;
|
||||
|
||||
private GpuContext _context;
|
||||
private Buffer _parent;
|
||||
private Action<ulong, ulong> _flushAction;
|
||||
|
||||
private List<BufferMigration> _sources;
|
||||
private BufferMigration _migrationTarget;
|
||||
|
||||
private object _lock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Whether the modified range list has any entries or not.
|
||||
/// </summary>
|
||||
public bool HasRanges
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return Count > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of a modified range list.
|
||||
/// </summary>
|
||||
/// <param name="context">GPU context that the buffer range list belongs to</param>
|
||||
public BufferModifiedRangeList(GpuContext context) : base(BackingInitialSize)
|
||||
/// <param name="parent">The parent buffer that owns this range list</param>
|
||||
/// <param name="flushAction">The flush action for the parent buffer</param>
|
||||
public BufferModifiedRangeList(GpuContext context, Buffer parent, Action<ulong, ulong> flushAction) : base(BackingInitialSize)
|
||||
{
|
||||
_context = context;
|
||||
_parent = parent;
|
||||
_flushAction = flushAction;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -142,6 +174,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
{
|
||||
// Region already exists. Just update the existing sync number.
|
||||
overlap.SyncNumber = syncNumber;
|
||||
overlap.Parent = this;
|
||||
|
||||
return;
|
||||
}
|
||||
@ -152,18 +185,18 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
{
|
||||
// A split item must be created behind this overlap.
|
||||
|
||||
Add(new BufferModifiedRange(overlap.Address, address - overlap.Address, overlap.SyncNumber));
|
||||
Add(new BufferModifiedRange(overlap.Address, address - overlap.Address, overlap.SyncNumber, overlap.Parent));
|
||||
}
|
||||
|
||||
if (overlap.Address < endAddress && overlap.EndAddress > endAddress)
|
||||
{
|
||||
// A split item must be created after this overlap.
|
||||
|
||||
Add(new BufferModifiedRange(endAddress, overlap.EndAddress - endAddress, overlap.SyncNumber));
|
||||
Add(new BufferModifiedRange(endAddress, overlap.EndAddress - endAddress, overlap.SyncNumber, overlap.Parent));
|
||||
}
|
||||
}
|
||||
|
||||
Add(new BufferModifiedRange(address, size, syncNumber));
|
||||
Add(new BufferModifiedRange(address, size, syncNumber, this));
|
||||
}
|
||||
}
|
||||
|
||||
@ -207,9 +240,102 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs the given range action, or one from a migration that overlaps and has not synced yet.
|
||||
/// </summary>
|
||||
/// <param name="offset">The offset to pass to the action</param>
|
||||
/// <param name="size">The size to pass to the action</param>
|
||||
/// <param name="syncNumber">The sync number that has been reached</param>
|
||||
/// <param name="parent">The modified range list that originally owned this range</param>
|
||||
/// <param name="rangeAction">The action to perform</param>
|
||||
public void RangeActionWithMigration(ulong offset, ulong size, ulong syncNumber, BufferModifiedRangeList parent, Action<ulong, ulong> rangeAction)
|
||||
{
|
||||
bool firstSource = true;
|
||||
|
||||
if (parent != this)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_sources != null)
|
||||
{
|
||||
foreach (BufferMigration source in _sources)
|
||||
{
|
||||
if (source.Overlaps(offset, size, syncNumber))
|
||||
{
|
||||
if (firstSource && !source.FullyMatches(offset, size))
|
||||
{
|
||||
// Perform this buffer's action first. The migrations will run after.
|
||||
rangeAction(offset, size);
|
||||
}
|
||||
|
||||
source.RangeActionWithMigration(offset, size, syncNumber, parent);
|
||||
|
||||
firstSource = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (firstSource)
|
||||
{
|
||||
// No overlapping migrations, or they are not meant for this range, flush the data using the given action.
|
||||
rangeAction(offset, size);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes modified ranges ready by the sync number from the list, and flushes their buffer data within a given address range.
|
||||
/// </summary>
|
||||
/// <param name="overlaps">Overlapping ranges to check</param>
|
||||
/// <param name="rangeCount">Number of overlapping ranges</param>
|
||||
/// <param name="highestDiff">The highest difference between an overlapping range's sync number and the current one</param>
|
||||
/// <param name="currentSync">The current sync number</param>
|
||||
/// <param name="address">The start address of the flush range</param>
|
||||
/// <param name="endAddress">The end address of the flush range</param>
|
||||
private void RemoveRangesAndFlush(
|
||||
BufferModifiedRange[] overlaps,
|
||||
int rangeCount,
|
||||
long highestDiff,
|
||||
ulong currentSync,
|
||||
ulong address,
|
||||
ulong endAddress)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_migrationTarget == null)
|
||||
{
|
||||
ulong waitSync = currentSync + (ulong)highestDiff;
|
||||
|
||||
for (int i = 0; i < rangeCount; i++)
|
||||
{
|
||||
BufferModifiedRange overlap = overlaps[i];
|
||||
|
||||
long diff = (long)(overlap.SyncNumber - currentSync);
|
||||
|
||||
if (diff <= highestDiff)
|
||||
{
|
||||
ulong clampAddress = Math.Max(address, overlap.Address);
|
||||
ulong clampEnd = Math.Min(endAddress, overlap.EndAddress);
|
||||
|
||||
ClearPart(overlap, clampAddress, clampEnd);
|
||||
|
||||
RangeActionWithMigration(clampAddress, clampEnd - clampAddress, waitSync, overlap.Parent, _flushAction);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// There is a migration target to call instead. This can't be changed after set so accessing it outside the lock is fine.
|
||||
|
||||
_migrationTarget.Destination.RemoveRangesAndFlush(overlaps, rangeCount, highestDiff, currentSync, address, endAddress);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets modified ranges within the specified region, waits on ones from a previous sync number,
|
||||
/// and then fires the given action for each range individually.
|
||||
/// and then fires the flush action for each range individually.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This function assumes it is called from the background thread.
|
||||
@ -218,8 +344,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
/// </remarks>
|
||||
/// <param name="address">Start address to query</param>
|
||||
/// <param name="size">Size to query</param>
|
||||
/// <param name="rangeAction">The action to call for each modified range</param>
|
||||
public void WaitForAndGetRanges(ulong address, ulong size, Action<ulong, ulong> rangeAction)
|
||||
public void WaitForAndFlushRanges(ulong address, ulong size)
|
||||
{
|
||||
ulong endAddress = address + size;
|
||||
ulong currentSync = _context.SyncNumber;
|
||||
@ -231,10 +356,23 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
// Range list must be consistent for this operation
|
||||
lock (_lock)
|
||||
{
|
||||
rangeCount = FindOverlapsNonOverlapping(address, size, ref overlaps);
|
||||
if (_migrationTarget != null)
|
||||
{
|
||||
rangeCount = -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
rangeCount = FindOverlapsNonOverlapping(address, size, ref overlaps);
|
||||
}
|
||||
}
|
||||
|
||||
if (rangeCount == 0)
|
||||
if (rangeCount == -1)
|
||||
{
|
||||
_migrationTarget.Destination.WaitForAndFlushRanges(address, size);
|
||||
|
||||
return;
|
||||
}
|
||||
else if (rangeCount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -264,47 +402,37 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
// Wait for the syncpoint.
|
||||
_context.Renderer.WaitSync(currentSync + (ulong)highestDiff);
|
||||
|
||||
// Flush and remove all regions with the older syncpoint.
|
||||
lock (_lock)
|
||||
{
|
||||
for (int i = 0; i < rangeCount; i++)
|
||||
{
|
||||
BufferModifiedRange overlap = overlaps[i];
|
||||
|
||||
long diff = (long)(overlap.SyncNumber - currentSync);
|
||||
|
||||
if (diff <= highestDiff)
|
||||
{
|
||||
ulong clampAddress = Math.Max(address, overlap.Address);
|
||||
ulong clampEnd = Math.Min(endAddress, overlap.EndAddress);
|
||||
|
||||
ClearPart(overlap, clampAddress, clampEnd);
|
||||
|
||||
rangeAction(clampAddress, clampEnd - clampAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
RemoveRangesAndFlush(overlaps, rangeCount, highestDiff, currentSync, address, endAddress);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inherit ranges from another modified range list.
|
||||
/// </summary>
|
||||
/// <param name="ranges">The range list to inherit from</param>
|
||||
/// <param name="rangeAction">The action to call for each modified range</param>
|
||||
public void InheritRanges(BufferModifiedRangeList ranges, Action<ulong, ulong> rangeAction)
|
||||
/// <param name="registerRangeAction">The action to call for each modified range</param>
|
||||
public void InheritRanges(BufferModifiedRangeList ranges, Action<ulong, ulong> registerRangeAction)
|
||||
{
|
||||
BufferModifiedRange[] inheritRanges;
|
||||
|
||||
lock (ranges._lock)
|
||||
{
|
||||
inheritRanges = ranges.ToArray();
|
||||
}
|
||||
BufferMigration migration = new(ranges._parent, ranges._flushAction, ranges, this, _context.SyncNumber);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (BufferModifiedRange range in inheritRanges)
|
||||
ranges._parent.IncrementReferenceCount();
|
||||
ranges._migrationTarget = migration;
|
||||
|
||||
_context.RegisterBufferMigration(migration);
|
||||
|
||||
inheritRanges = ranges.ToArray();
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
Add(range);
|
||||
(_sources ??= new List<BufferMigration>()).Add(migration);
|
||||
|
||||
foreach (BufferModifiedRange range in inheritRanges)
|
||||
{
|
||||
Add(range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -313,44 +441,20 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
{
|
||||
if (range.SyncNumber != currentSync)
|
||||
{
|
||||
rangeAction(range.Address, range.Size);
|
||||
registerRangeAction(range.Address, range.Size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls the given action for modified ranges that aren't from the current sync number.
|
||||
/// Removes a source buffer migration, indicating its copy has completed.
|
||||
/// </summary>
|
||||
/// <param name="rangeAction">The action to call for each modified range</param>
|
||||
public void ReregisterRanges(Action<ulong, ulong> rangeAction)
|
||||
/// <param name="migration">The migration to remove</param>
|
||||
public void RemoveMigration(BufferMigration migration)
|
||||
{
|
||||
ref var ranges = ref ThreadStaticArray<BufferModifiedRange>.Get();
|
||||
int count;
|
||||
|
||||
// Range list must be consistent for this operation.
|
||||
lock (_lock)
|
||||
{
|
||||
count = Count;
|
||||
if (ranges.Length < count)
|
||||
{
|
||||
Array.Resize(ref ranges, count);
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
foreach (BufferModifiedRange range in this)
|
||||
{
|
||||
ranges[i++] = range;
|
||||
}
|
||||
}
|
||||
|
||||
ulong currentSync = _context.SyncNumber;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
BufferModifiedRange range = ranges[i];
|
||||
if (range.SyncNumber != currentSync)
|
||||
{
|
||||
rangeAction(range.Address, range.Size);
|
||||
}
|
||||
_sources.Remove(migration);
|
||||
}
|
||||
}
|
||||
|
||||
@ -362,12 +466,12 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
|
||||
if (overlap.Address < address)
|
||||
{
|
||||
Add(new BufferModifiedRange(overlap.Address, address - overlap.Address, overlap.SyncNumber));
|
||||
Add(new BufferModifiedRange(overlap.Address, address - overlap.Address, overlap.SyncNumber, overlap.Parent));
|
||||
}
|
||||
|
||||
if (overlap.EndAddress > endAddress)
|
||||
{
|
||||
Add(new BufferModifiedRange(endAddress, overlap.EndAddress - endAddress, overlap.SyncNumber));
|
||||
Add(new BufferModifiedRange(endAddress, overlap.EndAddress - endAddress, overlap.SyncNumber, overlap.Parent));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -242,6 +242,19 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
WriteImpl(range, data, _cpuMemory.WriteUntracked);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes data to the application process, returning false if the data was not changed.
|
||||
/// This triggers read memory tracking, as a redundancy check would be useless if the data is not up to date.
|
||||
/// </summary>
|
||||
/// <remarks>The memory manager can return that memory has changed when it hasn't to avoid expensive data copies.</remarks>
|
||||
/// <param name="address">Address to write into</param>
|
||||
/// <param name="data">Data to be written</param>
|
||||
/// <returns>True if the data was changed, false otherwise</returns>
|
||||
public bool WriteWithRedundancyCheck(ulong address, ReadOnlySpan<byte> data)
|
||||
{
|
||||
return _cpuMemory.WriteWithRedundancyCheck(address, data);
|
||||
}
|
||||
|
||||
private delegate void WriteCallback(ulong address, ReadOnlySpan<byte> data);
|
||||
|
||||
/// <summary>
|
||||
|
@ -22,7 +22,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
|
||||
private const ushort FileFormatVersionMajor = 1;
|
||||
private const ushort FileFormatVersionMinor = 2;
|
||||
private const uint FileFormatVersionPacked = ((uint)FileFormatVersionMajor << 16) | FileFormatVersionMinor;
|
||||
private const uint CodeGenVersion = 3868;
|
||||
private const uint CodeGenVersion = 3943;
|
||||
|
||||
private const string SharedTocFileName = "shared.toc";
|
||||
private const string SharedDataFileName = "shared.data";
|
||||
|
@ -331,7 +331,7 @@ namespace Ryujinx.Graphics.OpenGL
|
||||
case PrimitiveTopology.QuadStrip:
|
||||
return PrimitiveType.QuadStrip;
|
||||
case PrimitiveTopology.Polygon:
|
||||
return PrimitiveType.Polygon;
|
||||
return PrimitiveType.TriangleFan;
|
||||
case PrimitiveTopology.LinesAdjacency:
|
||||
return PrimitiveType.LinesAdjacency;
|
||||
case PrimitiveTopology.LineStripAdjacency:
|
||||
|
@ -238,6 +238,11 @@ namespace Ryujinx.Graphics.OpenGL
|
||||
_sync.Wait(id);
|
||||
}
|
||||
|
||||
public ulong GetCurrentSync()
|
||||
{
|
||||
return _sync.GetCurrent();
|
||||
}
|
||||
|
||||
public void Screenshot()
|
||||
{
|
||||
_window.ScreenCaptureRequested = true;
|
||||
|
@ -1296,9 +1296,9 @@ namespace Ryujinx.Graphics.OpenGL
|
||||
_stencilFrontMask = stencilTest.FrontMask;
|
||||
}
|
||||
|
||||
public void SetStorageBuffers(int first, ReadOnlySpan<BufferRange> buffers)
|
||||
public void SetStorageBuffers(ReadOnlySpan<BufferAssignment> buffers)
|
||||
{
|
||||
SetBuffers(first, buffers, isStorage: true);
|
||||
SetBuffers(buffers, isStorage: true);
|
||||
}
|
||||
|
||||
public void SetTextureAndSampler(ShaderStage stage, int binding, ITexture texture, ISampler sampler)
|
||||
@ -1366,9 +1366,9 @@ namespace Ryujinx.Graphics.OpenGL
|
||||
}
|
||||
}
|
||||
|
||||
public void SetUniformBuffers(int first, ReadOnlySpan<BufferRange> buffers)
|
||||
public void SetUniformBuffers(ReadOnlySpan<BufferAssignment> buffers)
|
||||
{
|
||||
SetBuffers(first, buffers, isStorage: false);
|
||||
SetBuffers(buffers, isStorage: false);
|
||||
}
|
||||
|
||||
public void SetUserClipDistance(int index, bool enableClip)
|
||||
@ -1460,21 +1460,22 @@ namespace Ryujinx.Graphics.OpenGL
|
||||
GL.MemoryBarrier(MemoryBarrierFlags.TextureFetchBarrierBit);
|
||||
}
|
||||
|
||||
private void SetBuffers(int first, ReadOnlySpan<BufferRange> buffers, bool isStorage)
|
||||
private void SetBuffers(ReadOnlySpan<BufferAssignment> buffers, bool isStorage)
|
||||
{
|
||||
BufferRangeTarget target = isStorage ? BufferRangeTarget.ShaderStorageBuffer : BufferRangeTarget.UniformBuffer;
|
||||
|
||||
for (int index = 0; index < buffers.Length; index++)
|
||||
{
|
||||
BufferRange buffer = buffers[index];
|
||||
BufferAssignment assignment = buffers[index];
|
||||
BufferRange buffer = assignment.Range;
|
||||
|
||||
if (buffer.Handle == BufferHandle.Null)
|
||||
{
|
||||
GL.BindBufferRange(target, first + index, 0, IntPtr.Zero, 0);
|
||||
GL.BindBufferRange(target, assignment.Binding, 0, IntPtr.Zero, 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
GL.BindBufferRange(target, first + index, buffer.Handle.ToInt32(), (IntPtr)buffer.Offset, buffer.Size);
|
||||
GL.BindBufferRange(target, assignment.Binding, buffer.Handle.ToInt32(), (IntPtr)buffer.Offset, buffer.Size);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,4 +54,4 @@ namespace Ryujinx.Graphics.OpenGL.Queries
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="OpenTK.Graphics" Version="4.7.2" />
|
||||
<PackageReference Include="OpenTK.Graphics" Version="4.7.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -40,6 +40,37 @@ namespace Ryujinx.Graphics.OpenGL
|
||||
}
|
||||
}
|
||||
|
||||
public ulong GetCurrent()
|
||||
{
|
||||
lock (_handles)
|
||||
{
|
||||
ulong lastHandle = _firstHandle;
|
||||
|
||||
foreach (SyncHandle handle in _handles)
|
||||
{
|
||||
lock (handle)
|
||||
{
|
||||
if (handle.Handle == IntPtr.Zero)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (handle.ID > lastHandle)
|
||||
{
|
||||
WaitSyncStatus syncResult = GL.ClientWaitSync(handle.Handle, _syncFlags, 0);
|
||||
|
||||
if (syncResult == WaitSyncStatus.AlreadySignaled)
|
||||
{
|
||||
lastHandle = handle.ID;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lastHandle;
|
||||
}
|
||||
}
|
||||
|
||||
public void Wait(ulong id)
|
||||
{
|
||||
SyncHandle result = null;
|
||||
|
@ -10,13 +10,9 @@ namespace Ryujinx.Graphics.OpenGL
|
||||
{
|
||||
public int Handle { get; private set; }
|
||||
|
||||
private bool _needsAttribsUpdate;
|
||||
|
||||
private readonly VertexAttribDescriptor[] _vertexAttribs;
|
||||
private readonly VertexBufferDescriptor[] _vertexBuffers;
|
||||
|
||||
private int _vertexAttribsCount;
|
||||
private int _vertexBuffersCount;
|
||||
private int _minVertexCount;
|
||||
|
||||
private uint _vertexAttribsInUse;
|
||||
@ -76,9 +72,7 @@ namespace Ryujinx.Graphics.OpenGL
|
||||
_vertexBuffers[bindingIndex] = vb;
|
||||
}
|
||||
|
||||
_vertexBuffersCount = bindingIndex;
|
||||
_minVertexCount = minVertexCount;
|
||||
_needsAttribsUpdate = true;
|
||||
}
|
||||
|
||||
public void SetVertexAttributes(ReadOnlySpan<VertexAttribDescriptor> vertexAttribs)
|
||||
@ -131,8 +125,6 @@ namespace Ryujinx.Graphics.OpenGL
|
||||
_vertexAttribs[index] = attrib;
|
||||
}
|
||||
|
||||
_vertexAttribsCount = index;
|
||||
|
||||
for (; index < Constants.MaxVertexAttribs; index++)
|
||||
{
|
||||
DisableVertexAttrib(index);
|
||||
@ -160,13 +152,11 @@ namespace Ryujinx.Graphics.OpenGL
|
||||
public void PreDraw(int vertexCount)
|
||||
{
|
||||
LimitVertexBuffers(vertexCount);
|
||||
Validate();
|
||||
}
|
||||
|
||||
public void PreDrawVbUnbounded()
|
||||
{
|
||||
UnlimitVertexBuffers();
|
||||
Validate();
|
||||
}
|
||||
|
||||
public void LimitVertexBuffers(int vertexCount)
|
||||
@ -252,36 +242,6 @@ namespace Ryujinx.Graphics.OpenGL
|
||||
_vertexBuffersLimited = 0;
|
||||
}
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
for (int attribIndex = 0; attribIndex < _vertexAttribsCount; attribIndex++)
|
||||
{
|
||||
VertexAttribDescriptor attrib = _vertexAttribs[attribIndex];
|
||||
|
||||
if (!attrib.IsZero)
|
||||
{
|
||||
if ((uint)attrib.BufferIndex >= _vertexBuffersCount)
|
||||
{
|
||||
DisableVertexAttrib(attribIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_vertexBuffers[attrib.BufferIndex].Buffer.Handle == BufferHandle.Null)
|
||||
{
|
||||
DisableVertexAttrib(attribIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_needsAttribsUpdate)
|
||||
{
|
||||
EnableVertexAttrib(attribIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_needsAttribsUpdate = false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void EnableVertexAttrib(int index)
|
||||
{
|
||||
|
@ -10,6 +10,8 @@ namespace Ryujinx.Graphics.OpenGL
|
||||
private const int TextureCount = 3;
|
||||
private readonly OpenGLRenderer _renderer;
|
||||
|
||||
private bool _initialized;
|
||||
|
||||
private int _width;
|
||||
private int _height;
|
||||
private int _copyFramebufferHandle;
|
||||
@ -179,6 +181,7 @@ namespace Ryujinx.Graphics.OpenGL
|
||||
public void InitializeBackgroundContext(IOpenGLContext baseContext)
|
||||
{
|
||||
BackgroundContext = new BackgroundContextWorker(baseContext);
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
public void CaptureFrame(int x, int y, int width, int height, bool isBgra, bool flipX, bool flipY)
|
||||
@ -193,6 +196,11 @@ namespace Ryujinx.Graphics.OpenGL
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BackgroundContext.Dispose();
|
||||
|
||||
if (_copyFramebufferHandle != 0)
|
||||
@ -203,4 +211,4 @@ namespace Ryujinx.Graphics.OpenGL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -234,7 +234,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
|
||||
var source = operation.GetSource(0);
|
||||
|
||||
var uvec4Type = context.TypeVector(context.TypeU32(), 4);
|
||||
var execution = context.Constant(context.TypeU32(), 3); // Subgroup
|
||||
var execution = context.Constant(context.TypeU32(), Scope.Subgroup);
|
||||
|
||||
var maskVector = context.GroupNonUniformBallot(uvec4Type, execution, context.Get(AggregateType.Bool, source));
|
||||
var mask = context.CompositeExtract(context.TypeU32(), maskVector, (SpvLiteralInteger)0);
|
||||
@ -1233,7 +1233,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
|
||||
var maxThreadId = context.BitwiseOr(context.TypeU32(), minThreadId, clampNotSegMask);
|
||||
var srcThreadId = context.BitwiseOr(context.TypeU32(), indexNotSegMask, minThreadId);
|
||||
var valid = context.ULessThanEqual(context.TypeBool(), srcThreadId, maxThreadId);
|
||||
var value = context.SubgroupReadInvocationKHR(context.TypeFP32(), x, srcThreadId);
|
||||
var value = context.GroupNonUniformShuffle(context.TypeFP32(), context.Constant(context.TypeU32(), (int)Scope.Subgroup), x, srcThreadId);
|
||||
var result = context.Select(context.TypeFP32(), valid, value, x);
|
||||
|
||||
var validLocal = (AstOperand)operation.GetSource(3);
|
||||
@ -1263,7 +1263,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
|
||||
var maxThreadId = context.BitwiseOr(context.TypeU32(), minThreadId, clampNotSegMask);
|
||||
var srcThreadId = context.IAdd(context.TypeU32(), threadId, index);
|
||||
var valid = context.ULessThanEqual(context.TypeBool(), srcThreadId, maxThreadId);
|
||||
var value = context.SubgroupReadInvocationKHR(context.TypeFP32(), x, srcThreadId);
|
||||
var value = context.GroupNonUniformShuffle(context.TypeFP32(), context.Constant(context.TypeU32(), (int)Scope.Subgroup), x, srcThreadId);
|
||||
var result = context.Select(context.TypeFP32(), valid, value, x);
|
||||
|
||||
var validLocal = (AstOperand)operation.GetSource(3);
|
||||
@ -1289,7 +1289,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
|
||||
var minThreadId = context.BitwiseAnd(context.TypeU32(), threadId, segMask);
|
||||
var srcThreadId = context.ISub(context.TypeU32(), threadId, index);
|
||||
var valid = context.SGreaterThanEqual(context.TypeBool(), srcThreadId, minThreadId);
|
||||
var value = context.SubgroupReadInvocationKHR(context.TypeFP32(), x, srcThreadId);
|
||||
var value = context.GroupNonUniformShuffle(context.TypeFP32(), context.Constant(context.TypeU32(), (int)Scope.Subgroup), x, srcThreadId);
|
||||
var result = context.Select(context.TypeFP32(), valid, value, x);
|
||||
|
||||
var validLocal = (AstOperand)operation.GetSource(3);
|
||||
@ -1319,7 +1319,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
|
||||
var maxThreadId = context.BitwiseOr(context.TypeU32(), minThreadId, clampNotSegMask);
|
||||
var srcThreadId = context.BitwiseXor(context.TypeU32(), threadId, index);
|
||||
var valid = context.ULessThanEqual(context.TypeBool(), srcThreadId, maxThreadId);
|
||||
var value = context.SubgroupReadInvocationKHR(context.TypeFP32(), x, srcThreadId);
|
||||
var value = context.GroupNonUniformShuffle(context.TypeFP32(), context.Constant(context.TypeU32(), (int)Scope.Subgroup), x, srcThreadId);
|
||||
var result = context.Select(context.TypeFP32(), valid, value, x);
|
||||
|
||||
var validLocal = (AstOperand)operation.GetSource(3);
|
||||
@ -1861,19 +1861,22 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
|
||||
|
||||
private static OperationResult GenerateVoteAll(CodeGenContext context, AstOperation operation)
|
||||
{
|
||||
var result = context.SubgroupAllKHR(context.TypeBool(), context.Get(AggregateType.Bool, operation.GetSource(0)));
|
||||
var execution = context.Constant(context.TypeU32(), Scope.Subgroup);
|
||||
var result = context.GroupNonUniformAll(context.TypeBool(), execution, context.Get(AggregateType.Bool, operation.GetSource(0)));
|
||||
return new OperationResult(AggregateType.Bool, result);
|
||||
}
|
||||
|
||||
private static OperationResult GenerateVoteAllEqual(CodeGenContext context, AstOperation operation)
|
||||
{
|
||||
var result = context.SubgroupAllEqualKHR(context.TypeBool(), context.Get(AggregateType.Bool, operation.GetSource(0)));
|
||||
var execution = context.Constant(context.TypeU32(), Scope.Subgroup);
|
||||
var result = context.GroupNonUniformAllEqual(context.TypeBool(), execution, context.Get(AggregateType.Bool, operation.GetSource(0)));
|
||||
return new OperationResult(AggregateType.Bool, result);
|
||||
}
|
||||
|
||||
private static OperationResult GenerateVoteAny(CodeGenContext context, AstOperation operation)
|
||||
{
|
||||
var result = context.SubgroupAnyKHR(context.TypeBool(), context.Get(AggregateType.Bool, operation.GetSource(0)));
|
||||
var execution = context.Constant(context.TypeU32(), Scope.Subgroup);
|
||||
var result = context.GroupNonUniformAny(context.TypeBool(), execution, context.Get(AggregateType.Bool, operation.GetSource(0)));
|
||||
return new OperationResult(AggregateType.Bool, result);
|
||||
}
|
||||
|
||||
|
@ -50,12 +50,12 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
|
||||
CodeGenContext context = new CodeGenContext(info, config, instPool, integerPool);
|
||||
|
||||
context.AddCapability(Capability.GroupNonUniformBallot);
|
||||
context.AddCapability(Capability.GroupNonUniformShuffle);
|
||||
context.AddCapability(Capability.GroupNonUniformVote);
|
||||
context.AddCapability(Capability.ImageBuffer);
|
||||
context.AddCapability(Capability.ImageGatherExtended);
|
||||
context.AddCapability(Capability.ImageQuery);
|
||||
context.AddCapability(Capability.SampledBuffer);
|
||||
context.AddCapability(Capability.SubgroupBallotKHR);
|
||||
context.AddCapability(Capability.SubgroupVoteKHR);
|
||||
|
||||
if (config.TransformFeedbackEnabled && config.LastInVertexPipeline)
|
||||
{
|
||||
@ -94,9 +94,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
|
||||
context.AddCapability(Capability.DrawParameters);
|
||||
}
|
||||
|
||||
context.AddExtension("SPV_KHR_shader_ballot");
|
||||
context.AddExtension("SPV_KHR_subgroup_vote");
|
||||
|
||||
Declarations.DeclareAll(context, info);
|
||||
|
||||
if ((info.HelperFunctionsMask & NeedsInvocationIdMask) != 0)
|
||||
|
@ -128,6 +128,11 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
|
||||
GetStorageOffset(block, Utils.FindLastOperation(addrLow, block), baseAddressCbOffset) :
|
||||
(null, 0);
|
||||
|
||||
if (byteOffset != null)
|
||||
{
|
||||
ReplaceAddressAlignment(node.List, addrLow, byteOffset, constantOffset);
|
||||
}
|
||||
|
||||
if (byteOffset == null)
|
||||
{
|
||||
Operand baseAddrLow = Cbuf(0, baseAddressCbOffset);
|
||||
@ -156,11 +161,6 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
|
||||
byteOffset = offset;
|
||||
}
|
||||
|
||||
if (byteOffset != null)
|
||||
{
|
||||
ReplaceAddressAlignment(node.List, addrLow, byteOffset, constantOffset);
|
||||
}
|
||||
|
||||
if (isStg16Or8)
|
||||
{
|
||||
return byteOffset;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user