Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
815819767c | ||
|
623604c391 | ||
|
617c5700ca | ||
|
7b62f7475e | ||
|
841dd56f4c | ||
|
a16d582a10 | ||
|
9ef0be477b |
@@ -38,7 +38,9 @@ namespace ARMeilleure.Decoders
|
||||
{
|
||||
block = new Block(blkAddress);
|
||||
|
||||
if ((dMode != DecoderMode.MultipleBlocks && visited.Count >= 1) || opsCount > instructionLimit || !memory.IsMapped(blkAddress))
|
||||
if ((dMode != DecoderMode.MultipleBlocks && visited.Count >= 1) ||
|
||||
opsCount > instructionLimit ||
|
||||
(visited.Count > 0 && !memory.IsMapped(blkAddress)))
|
||||
{
|
||||
block.Exit = true;
|
||||
block.EndAddress = blkAddress;
|
||||
|
@@ -716,7 +716,7 @@ namespace Ryujinx.Ava
|
||||
|
||||
ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, appMetadata =>
|
||||
{
|
||||
appMetadata.LastPlayed = DateTime.UtcNow;
|
||||
appMetadata.UpdatePreGame();
|
||||
});
|
||||
|
||||
return true;
|
||||
|
@@ -6,13 +6,13 @@ using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.GraphicsDriver;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.SystemInfo;
|
||||
using Ryujinx.Common.SystemInterop;
|
||||
using Ryujinx.Modules;
|
||||
using Ryujinx.SDL2.Common;
|
||||
using Ryujinx.Ui.Common;
|
||||
using Ryujinx.Ui.Common.Configuration;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using Ryujinx.Ui.Common.SystemInfo;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
@@ -126,17 +126,17 @@
|
||||
Spacing="5">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding TimePlayed}"
|
||||
Text="{Binding TimePlayedString}"
|
||||
TextAlignment="Right"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding LastPlayed, Converter={helpers:NullableDateTimeConverter}}"
|
||||
Text="{Binding LastPlayedString, Converter={helpers:LocalizedNeverConverter}}"
|
||||
TextAlignment="Right"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding FileSize}"
|
||||
Text="{Binding FileSizeString}"
|
||||
TextAlignment="Right"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
|
43
src/Ryujinx.Ava/UI/Helpers/LocalizedNeverConverter.cs
Normal file
43
src/Ryujinx.Ava/UI/Helpers/LocalizedNeverConverter.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// This <see cref="IValueConverter"/> makes sure that the string "Never" that's returned by <see cref="ValueFormatUtils.FormatDateTime"/> is properly localized in the Avalonia UI.
|
||||
/// After the Avalonia UI has been made the default and the GTK UI is removed, <see cref="ValueFormatUtils"/> should be updated to directly return a localized string.
|
||||
/// </summary>
|
||||
internal class LocalizedNeverConverter : MarkupExtension, IValueConverter
|
||||
{
|
||||
private static readonly LocalizedNeverConverter _instance = new();
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is not string valStr)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
if (valStr == "Never")
|
||||
{
|
||||
return LocaleManager.Instance[LocaleKeys.Never];
|
||||
}
|
||||
|
||||
return valStr;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override object ProvideValue(IServiceProvider serviceProvider)
|
||||
{
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,38 +0,0 @@
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
internal class NullableDateTimeConverter : MarkupExtension, IValueConverter
|
||||
{
|
||||
private static readonly NullableDateTimeConverter _instance = new();
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return LocaleManager.Instance[LocaleKeys.Never];
|
||||
}
|
||||
|
||||
if (value is DateTime dateTime)
|
||||
{
|
||||
return dateTime.ToLocalTime().ToString(culture);
|
||||
}
|
||||
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override object ProvideValue(IServiceProvider serviceProvider)
|
||||
{
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
}
|
@@ -13,20 +13,19 @@ namespace Ryujinx.Ava.UI.Models.Generic
|
||||
|
||||
public int Compare(ApplicationData x, ApplicationData y)
|
||||
{
|
||||
var aValue = x.LastPlayed;
|
||||
var bValue = y.LastPlayed;
|
||||
DateTime aValue = DateTime.UnixEpoch, bValue = DateTime.UnixEpoch;
|
||||
|
||||
if (!aValue.HasValue)
|
||||
if (x?.LastPlayed != null)
|
||||
{
|
||||
aValue = DateTime.UnixEpoch;
|
||||
aValue = x.LastPlayed.Value;
|
||||
}
|
||||
|
||||
if (!bValue.HasValue)
|
||||
if (y?.LastPlayed != null)
|
||||
{
|
||||
bValue = DateTime.UnixEpoch;
|
||||
bValue = y.LastPlayed.Value;
|
||||
}
|
||||
|
||||
return (IsAscending ? 1 : -1) * DateTime.Compare(bValue.Value, aValue.Value);
|
||||
return (IsAscending ? 1 : -1) * DateTime.Compare(aValue, bValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
31
src/Ryujinx.Ava/UI/Models/Generic/TimePlayedSortComparer.cs
Normal file
31
src/Ryujinx.Ava/UI/Models/Generic/TimePlayedSortComparer.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Ryujinx.Ui.App.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Models.Generic
|
||||
{
|
||||
internal class TimePlayedSortComparer : IComparer<ApplicationData>
|
||||
{
|
||||
public TimePlayedSortComparer() { }
|
||||
public TimePlayedSortComparer(bool isAscending) { IsAscending = isAscending; }
|
||||
|
||||
public bool IsAscending { get; }
|
||||
|
||||
public int Compare(ApplicationData x, ApplicationData y)
|
||||
{
|
||||
TimeSpan aValue = TimeSpan.Zero, bValue = TimeSpan.Zero;
|
||||
|
||||
if (x?.TimePlayed != null)
|
||||
{
|
||||
aValue = x.TimePlayed;
|
||||
}
|
||||
|
||||
if (y?.TimePlayed != null)
|
||||
{
|
||||
bValue = y.TimePlayed;
|
||||
}
|
||||
|
||||
return (IsAscending ? 1 : -1) * TimeSpan.Compare(aValue, bValue);
|
||||
}
|
||||
}
|
||||
}
|
@@ -4,7 +4,7 @@ using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.Ui.App.Common;
|
||||
using System;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@@ -38,26 +38,7 @@ namespace Ryujinx.Ava.UI.Models
|
||||
|
||||
public bool SizeAvailable { get; set; }
|
||||
|
||||
public string SizeString => GetSizeString();
|
||||
|
||||
private string GetSizeString()
|
||||
{
|
||||
const int Scale = 1024;
|
||||
string[] orders = { "GiB", "MiB", "KiB" };
|
||||
long max = (long)Math.Pow(Scale, orders.Length);
|
||||
|
||||
foreach (string order in orders)
|
||||
{
|
||||
if (Size > max)
|
||||
{
|
||||
return $"{decimal.Divide(Size, max):##.##} {order}";
|
||||
}
|
||||
|
||||
max /= Scale;
|
||||
}
|
||||
|
||||
return "0 KiB";
|
||||
}
|
||||
public string SizeString => ValueFormatUtils.FormatFileSize(Size);
|
||||
|
||||
public SaveModel(SaveDataInfo info)
|
||||
{
|
||||
|
@@ -930,21 +930,20 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
return SortMode switch
|
||||
{
|
||||
#pragma warning disable IDE0055 // Disable formatting
|
||||
ApplicationSort.Title => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.TitleName)
|
||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.TitleName),
|
||||
ApplicationSort.Developer => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Developer)
|
||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.Developer),
|
||||
ApplicationSort.LastPlayed => new LastPlayedSortComparer(IsAscending),
|
||||
ApplicationSort.FileSize => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.FileSizeBytes)
|
||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.FileSizeBytes),
|
||||
ApplicationSort.TotalTimePlayed => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.TimePlayedNum)
|
||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.TimePlayedNum),
|
||||
ApplicationSort.Title => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.TitleName)
|
||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.TitleName),
|
||||
ApplicationSort.TotalTimePlayed => new TimePlayedSortComparer(IsAscending),
|
||||
ApplicationSort.FileType => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.FileExtension)
|
||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.FileExtension),
|
||||
ApplicationSort.FileSize => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.FileSize)
|
||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.FileSize),
|
||||
ApplicationSort.Path => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Path)
|
||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.Path),
|
||||
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,
|
||||
#pragma warning restore IDE0055
|
||||
};
|
||||
@@ -1549,13 +1548,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata =>
|
||||
{
|
||||
if (appMetadata.LastPlayed.HasValue)
|
||||
{
|
||||
double sessionTimePlayed = DateTime.UtcNow.Subtract(appMetadata.LastPlayed.Value).TotalSeconds;
|
||||
appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
appMetadata.LastPlayed = DateTime.UtcNow;
|
||||
appMetadata.UpdatePostGame();
|
||||
});
|
||||
}
|
||||
|
||||
|
62
src/Ryujinx.Cpu/AppleHv/HvCodePatcher.cs
Normal file
62
src/Ryujinx.Cpu/AppleHv/HvCodePatcher.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Intrinsics;
|
||||
|
||||
namespace Ryujinx.Cpu.AppleHv
|
||||
{
|
||||
static class HvCodePatcher
|
||||
{
|
||||
private const uint XMask = 0x3f808000u;
|
||||
private const uint XValue = 0x8000000u;
|
||||
|
||||
private const uint ZrIndex = 31u;
|
||||
|
||||
public static void RewriteUnorderedExclusiveInstructions(Span<byte> code)
|
||||
{
|
||||
Span<uint> codeUint = MemoryMarshal.Cast<byte, uint>(code);
|
||||
Span<Vector128<uint>> codeVector = MemoryMarshal.Cast<byte, Vector128<uint>>(code);
|
||||
|
||||
Vector128<uint> mask = Vector128.Create(XMask);
|
||||
Vector128<uint> value = Vector128.Create(XValue);
|
||||
|
||||
for (int index = 0; index < codeVector.Length; index++)
|
||||
{
|
||||
Vector128<uint> v = codeVector[index];
|
||||
|
||||
if (Vector128.EqualsAny(Vector128.BitwiseAnd(v, mask), value))
|
||||
{
|
||||
int baseIndex = index * 4;
|
||||
|
||||
for (int instIndex = baseIndex; instIndex < baseIndex + 4; instIndex++)
|
||||
{
|
||||
ref uint inst = ref codeUint[instIndex];
|
||||
|
||||
if ((inst & XMask) != XValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool isPair = (inst & (1u << 21)) != 0;
|
||||
bool isLoad = (inst & (1u << 22)) != 0;
|
||||
|
||||
uint rt2 = (inst >> 10) & 0x1fu;
|
||||
uint rs = (inst >> 16) & 0x1fu;
|
||||
|
||||
if (isLoad && rs != ZrIndex)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isPair && rt2 != ZrIndex)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Set the ordered flag.
|
||||
inst |= 1u << 15;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -128,21 +128,6 @@ namespace Ryujinx.Cpu.AppleHv
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable IDE0051 // Remove unused private member
|
||||
/// <summary>
|
||||
/// Ensures the combination of virtual address and size is part of the addressable space and fully mapped.
|
||||
/// </summary>
|
||||
/// <param name="va">Virtual address of the range</param>
|
||||
/// <param name="size">Size of the range in bytes</param>
|
||||
private void AssertMapped(ulong va, ulong size)
|
||||
{
|
||||
if (!ValidateAddressAndSize(va, size) || !IsRangeMappedImpl(va, size))
|
||||
{
|
||||
throw new InvalidMemoryRegionException($"Not mapped: va=0x{va:X16}, size=0x{size:X16}");
|
||||
}
|
||||
}
|
||||
#pragma warning restore IDE0051
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Map(ulong va, ulong pa, ulong size, MemoryMapFlags flags)
|
||||
{
|
||||
@@ -736,6 +721,24 @@ namespace Ryujinx.Cpu.AppleHv
|
||||
return (int)(vaSpan / PageSize);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Reprotect(ulong va, ulong size, MemoryPermission protection)
|
||||
{
|
||||
if (protection.HasFlag(MemoryPermission.Execute))
|
||||
{
|
||||
// Some applications use unordered exclusive memory access instructions
|
||||
// where it is not valid to do so, leading to memory re-ordering that
|
||||
// makes the code behave incorrectly on some CPUs.
|
||||
// To work around this, we force all such accesses to be ordered.
|
||||
|
||||
using WritableRegion writableRegion = GetWritableRegion(va, (int)size);
|
||||
|
||||
HvCodePatcher.RewriteUnorderedExclusiveInstructions(writableRegion.Memory.Span);
|
||||
}
|
||||
|
||||
// TODO
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
|
||||
{
|
||||
|
@@ -575,24 +575,17 @@ namespace Ryujinx.Cpu.Jit
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable IDE0051 // Remove unused private member
|
||||
private ulong GetPhysicalAddress(ulong va)
|
||||
{
|
||||
// We return -1L if the virtual address is invalid or unmapped.
|
||||
if (!ValidateAddress(va) || !IsMapped(va))
|
||||
{
|
||||
return ulong.MaxValue;
|
||||
}
|
||||
|
||||
return GetPhysicalAddressInternal(va);
|
||||
}
|
||||
#pragma warning restore IDE0051
|
||||
|
||||
private ulong GetPhysicalAddressInternal(ulong va)
|
||||
{
|
||||
return PteToPa(_pageTable.Read<ulong>((va / PageSize) * PteSize) & ~(0xffffUL << 48)) + (va & PageMask);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Reprotect(ulong va, ulong size, MemoryPermission protection)
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
|
||||
{
|
||||
@@ -698,9 +691,5 @@ namespace Ryujinx.Cpu.Jit
|
||||
/// Disposes of resources used by the memory manager.
|
||||
/// </summary>
|
||||
protected override void Destroy() => _pageTable.Dispose();
|
||||
|
||||
#pragma warning disable IDE0051 // Remove unused private member
|
||||
private static void ThrowInvalidMemoryRegionException(string message) => throw new InvalidMemoryRegionException(message);
|
||||
#pragma warning restore IDE0051
|
||||
}
|
||||
}
|
||||
|
@@ -615,6 +615,12 @@ namespace Ryujinx.Cpu.Jit
|
||||
return (int)(vaSpan / PageSize);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Reprotect(ulong va, ulong size, MemoryPermission protection)
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
|
||||
{
|
||||
|
@@ -101,6 +101,11 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// </summary>
|
||||
public bool AlwaysFlushOnOverlap { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the texture was fully unmapped since the modified flag was set, and flushes should be ignored until it is modified again.
|
||||
/// </summary>
|
||||
public bool FlushStale { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Increments when the host texture is swapped, or when the texture is removed from all pools.
|
||||
/// </summary>
|
||||
@@ -149,6 +154,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// </summary>
|
||||
public bool HadPoolOwner { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Physical memory ranges where the texture data is located.
|
||||
/// </summary>
|
||||
public MultiRange Range { get; private set; }
|
||||
@@ -1411,6 +1417,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// </summary>
|
||||
public void SignalModified()
|
||||
{
|
||||
FlushStale = false;
|
||||
_scaledSetScore = Math.Max(0, _scaledSetScore - 1);
|
||||
|
||||
if (_modifiedStale || Group.HasCopyDependencies)
|
||||
@@ -1431,6 +1438,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
if (bound)
|
||||
{
|
||||
FlushStale = false;
|
||||
_scaledSetScore = Math.Max(0, _scaledSetScore - 1);
|
||||
}
|
||||
|
||||
@@ -1695,12 +1703,17 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// <param name="unmapRange">The range of memory being unmapped</param>
|
||||
public void Unmapped(MultiRange unmapRange)
|
||||
{
|
||||
if (unmapRange.Contains(Range))
|
||||
{
|
||||
// If this is a full unmap, prevent flushes until the texture is mapped again.
|
||||
FlushStale = true;
|
||||
}
|
||||
|
||||
ChangedMapping = true;
|
||||
|
||||
if (Group.Storage == this)
|
||||
{
|
||||
Group.Unmapped();
|
||||
|
||||
Group.ClearModified(unmapRange);
|
||||
}
|
||||
}
|
||||
|
@@ -107,8 +107,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
// Any texture that has been unmapped at any point or is partially unmapped
|
||||
// should update their pool references after the remap completes.
|
||||
|
||||
MultiRange unmapped = ((MemoryManager)sender).GetPhysicalRegions(e.Address, e.Size);
|
||||
|
||||
foreach (var texture in _partiallyMappedTextures)
|
||||
{
|
||||
texture.UpdatePoolMappings();
|
||||
@@ -735,9 +733,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
if (overlap.IsView)
|
||||
{
|
||||
overlapCompatibility = overlapCompatibility == TextureViewCompatibility.FormatAlias ?
|
||||
TextureViewCompatibility.Incompatible :
|
||||
TextureViewCompatibility.CopyOnly;
|
||||
overlapCompatibility = TextureViewCompatibility.CopyOnly;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -815,7 +811,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
Texture overlap = _textureOverlaps[index];
|
||||
OverlapInfo oInfo = _overlapInfo[index];
|
||||
|
||||
if (oInfo.Compatibility <= TextureViewCompatibility.LayoutIncompatible || oInfo.Compatibility == TextureViewCompatibility.FormatAlias)
|
||||
if (oInfo.Compatibility <= TextureViewCompatibility.LayoutIncompatible)
|
||||
{
|
||||
if (!overlap.IsView && texture.DataOverlaps(overlap, oInfo.Compatibility))
|
||||
{
|
||||
|
@@ -226,7 +226,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
// D32F and R32F texture have the same representation internally,
|
||||
// however the R32F format is used to sample from depth textures.
|
||||
if (lhs.FormatInfo.Format == Format.D32Float && rhs.FormatInfo.Format == Format.R32Float && (forSampler || depthAlias))
|
||||
if (IsValidDepthAsColorAlias(lhs.FormatInfo.Format, rhs.FormatInfo.Format) && (forSampler || depthAlias))
|
||||
{
|
||||
return TextureMatchQuality.FormatAlias;
|
||||
}
|
||||
@@ -239,14 +239,8 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
return TextureMatchQuality.FormatAlias;
|
||||
}
|
||||
|
||||
if (lhs.FormatInfo.Format == Format.D16Unorm && rhs.FormatInfo.Format == Format.R16Unorm)
|
||||
{
|
||||
return TextureMatchQuality.FormatAlias;
|
||||
}
|
||||
|
||||
if ((lhs.FormatInfo.Format == Format.D24UnormS8Uint ||
|
||||
lhs.FormatInfo.Format == Format.S8UintD24Unorm) && rhs.FormatInfo.Format == Format.B8G8R8A8Unorm)
|
||||
else if ((lhs.FormatInfo.Format == Format.D24UnormS8Uint ||
|
||||
lhs.FormatInfo.Format == Format.S8UintD24Unorm) && rhs.FormatInfo.Format == Format.B8G8R8A8Unorm)
|
||||
{
|
||||
return TextureMatchQuality.FormatAlias;
|
||||
}
|
||||
@@ -632,12 +626,27 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
|
||||
if (lhsFormat.Format.IsDepthOrStencil() || rhsFormat.Format.IsDepthOrStencil())
|
||||
{
|
||||
return FormatMatches(lhs, rhs, flags.HasFlag(TextureSearchFlags.ForSampler), flags.HasFlag(TextureSearchFlags.DepthAlias)) switch
|
||||
bool forSampler = flags.HasFlag(TextureSearchFlags.ForSampler);
|
||||
bool depthAlias = flags.HasFlag(TextureSearchFlags.DepthAlias);
|
||||
|
||||
TextureMatchQuality matchQuality = FormatMatches(lhs, rhs, forSampler, depthAlias);
|
||||
|
||||
if (matchQuality == TextureMatchQuality.Perfect)
|
||||
{
|
||||
TextureMatchQuality.Perfect => TextureViewCompatibility.Full,
|
||||
TextureMatchQuality.FormatAlias => TextureViewCompatibility.FormatAlias,
|
||||
_ => TextureViewCompatibility.Incompatible,
|
||||
};
|
||||
return TextureViewCompatibility.Full;
|
||||
}
|
||||
else if (matchQuality == TextureMatchQuality.FormatAlias)
|
||||
{
|
||||
return TextureViewCompatibility.FormatAlias;
|
||||
}
|
||||
else if (IsValidColorAsDepthAlias(lhsFormat.Format, rhsFormat.Format) || IsValidDepthAsColorAlias(lhsFormat.Format, rhsFormat.Format))
|
||||
{
|
||||
return TextureViewCompatibility.CopyOnly;
|
||||
}
|
||||
else
|
||||
{
|
||||
return TextureViewCompatibility.Incompatible;
|
||||
}
|
||||
}
|
||||
|
||||
if (IsFormatHostIncompatible(lhs, caps) || IsFormatHostIncompatible(rhs, caps))
|
||||
@@ -666,6 +675,30 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
return TextureViewCompatibility.Incompatible;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if it's valid to alias a color format as a depth format.
|
||||
/// </summary>
|
||||
/// <param name="lhsFormat">Source format to be checked</param>
|
||||
/// <param name="rhsFormat">Target format to be checked</param>
|
||||
/// <returns>True if it's valid to alias the formats</returns>
|
||||
private static bool IsValidColorAsDepthAlias(Format lhsFormat, Format rhsFormat)
|
||||
{
|
||||
return (lhsFormat == Format.R32Float && rhsFormat == Format.D32Float) ||
|
||||
(lhsFormat == Format.R16Unorm && rhsFormat == Format.D16Unorm);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if it's valid to alias a depth format as a color format.
|
||||
/// </summary>
|
||||
/// <param name="lhsFormat">Source format to be checked</param>
|
||||
/// <param name="rhsFormat">Target format to be checked</param>
|
||||
/// <returns>True if it's valid to alias the formats</returns>
|
||||
private static bool IsValidDepthAsColorAlias(Format lhsFormat, Format rhsFormat)
|
||||
{
|
||||
return (lhsFormat == Format.D32Float && rhsFormat == Format.R32Float) ||
|
||||
(lhsFormat == Format.D16Unorm && rhsFormat == Format.R16Unorm);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if aliasing of two formats that would normally be considered incompatible be allowed,
|
||||
/// using copy dependencies.
|
||||
|
@@ -1659,6 +1659,14 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
return;
|
||||
}
|
||||
|
||||
// If size is zero, we have nothing to flush.
|
||||
// If the flush is stale, we should ignore it because the texture was unmapped since the modified
|
||||
// flag was set, and flushing it is not safe anymore as the GPU might no longer own the memory.
|
||||
if (size == 0 || Storage.FlushStale)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// There is a small gap here where the action is removed but _actionRegistered is still 1.
|
||||
// In this case it will skip registering the action, but here we are already handling it,
|
||||
// so there shouldn't be any issue as it's the same handler for all actions.
|
||||
|
@@ -367,7 +367,7 @@ namespace Ryujinx.Graphics.OpenGL.Image
|
||||
return to;
|
||||
}
|
||||
|
||||
private TextureView PboCopy(TextureView from, TextureView to, int srcLayer, int dstLayer, int srcLevel, int dstLevel, int width, int height)
|
||||
public void PboCopy(TextureView from, TextureView to, int srcLayer, int dstLayer, int srcLevel, int dstLevel, int width, int height)
|
||||
{
|
||||
int dstWidth = width;
|
||||
int dstHeight = height;
|
||||
@@ -445,8 +445,6 @@ namespace Ryujinx.Graphics.OpenGL.Image
|
||||
}
|
||||
|
||||
GL.BindBuffer(BufferTarget.PixelUnpackBuffer, 0);
|
||||
|
||||
return to;
|
||||
}
|
||||
|
||||
private void EnsurePbo(TextureView view)
|
||||
|
@@ -140,6 +140,28 @@ namespace Ryujinx.Graphics.OpenGL.Image
|
||||
int levels = Math.Min(Info.Levels, destinationView.Info.Levels - firstLevel);
|
||||
_renderer.TextureCopyIncompatible.CopyIncompatibleFormats(this, destinationView, 0, firstLayer, 0, firstLevel, layers, levels);
|
||||
}
|
||||
else if (destinationView.Format.IsDepthOrStencil() != Format.IsDepthOrStencil())
|
||||
{
|
||||
int layers = Math.Min(Info.GetLayers(), destinationView.Info.GetLayers() - firstLayer);
|
||||
int levels = Math.Min(Info.Levels, destinationView.Info.Levels - firstLevel);
|
||||
|
||||
for (int level = 0; level < levels; level++)
|
||||
{
|
||||
int srcWidth = Math.Max(1, Width >> level);
|
||||
int srcHeight = Math.Max(1, Height >> level);
|
||||
|
||||
int dstWidth = Math.Max(1, destinationView.Width >> (firstLevel + level));
|
||||
int dstHeight = Math.Max(1, destinationView.Height >> (firstLevel + level));
|
||||
|
||||
int minWidth = Math.Min(srcWidth, dstWidth);
|
||||
int minHeight = Math.Min(srcHeight, dstHeight);
|
||||
|
||||
for (int layer = 0; layer < layers; layer++)
|
||||
{
|
||||
_renderer.TextureCopy.PboCopy(this, destinationView, 0, firstLayer + layer, 0, firstLevel + level, minWidth, minHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_renderer.TextureCopy.CopyUnscaled(this, destinationView, 0, firstLayer, 0, firstLevel);
|
||||
@@ -169,6 +191,13 @@ namespace Ryujinx.Graphics.OpenGL.Image
|
||||
{
|
||||
_renderer.TextureCopyIncompatible.CopyIncompatibleFormats(this, destinationView, srcLayer, dstLayer, srcLevel, dstLevel, 1, 1);
|
||||
}
|
||||
else if (destinationView.Format.IsDepthOrStencil() != Format.IsDepthOrStencil())
|
||||
{
|
||||
int minWidth = Math.Min(Width, destinationView.Width);
|
||||
int minHeight = Math.Min(Height, destinationView.Height);
|
||||
|
||||
_renderer.TextureCopy.PboCopy(this, destinationView, srcLayer, dstLayer, srcLevel, dstLevel, minWidth, minHeight);
|
||||
}
|
||||
else
|
||||
{
|
||||
_renderer.TextureCopy.CopyUnscaled(this, destinationView, srcLayer, dstLayer, srcLevel, dstLevel, 1, 1);
|
||||
|
@@ -211,6 +211,13 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
int levels = Math.Min(Info.Levels, dst.Info.Levels - firstLevel);
|
||||
_gd.HelperShader.CopyIncompatibleFormats(_gd, cbs, src, dst, 0, firstLayer, 0, firstLevel, layers, levels);
|
||||
}
|
||||
else if (src.Info.Format.IsDepthOrStencil() != dst.Info.Format.IsDepthOrStencil())
|
||||
{
|
||||
int layers = Math.Min(Info.GetLayers(), dst.Info.GetLayers() - firstLayer);
|
||||
int levels = Math.Min(Info.Levels, dst.Info.Levels - firstLevel);
|
||||
|
||||
_gd.HelperShader.CopyColor(_gd, cbs, src, dst, 0, firstLayer, 0, FirstLevel, layers, levels);
|
||||
}
|
||||
else
|
||||
{
|
||||
TextureCopy.Copy(
|
||||
@@ -260,6 +267,10 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
{
|
||||
_gd.HelperShader.CopyIncompatibleFormats(_gd, cbs, src, dst, srcLayer, dstLayer, srcLevel, dstLevel, 1, 1);
|
||||
}
|
||||
else if (src.Info.Format.IsDepthOrStencil() != dst.Info.Format.IsDepthOrStencil())
|
||||
{
|
||||
_gd.HelperShader.CopyColor(_gd, cbs, src, dst, srcLayer, dstLayer, srcLevel, dstLevel, 1, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
TextureCopy.Copy(
|
||||
|
46
src/Ryujinx.HLE/HOS/Kernel/Memory/KMemoryPermission.cs
Normal file
46
src/Ryujinx.HLE/HOS/Kernel/Memory/KMemoryPermission.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using Ryujinx.Memory;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Kernel.Memory
|
||||
{
|
||||
[Flags]
|
||||
enum KMemoryPermission : uint
|
||||
{
|
||||
None = 0,
|
||||
UserMask = Read | Write | Execute,
|
||||
Mask = uint.MaxValue,
|
||||
|
||||
Read = 1 << 0,
|
||||
Write = 1 << 1,
|
||||
Execute = 1 << 2,
|
||||
DontCare = 1 << 28,
|
||||
|
||||
ReadAndWrite = Read | Write,
|
||||
ReadAndExecute = Read | Execute,
|
||||
}
|
||||
|
||||
static class KMemoryPermissionExtensions
|
||||
{
|
||||
public static MemoryPermission Convert(this KMemoryPermission permission)
|
||||
{
|
||||
MemoryPermission output = MemoryPermission.None;
|
||||
|
||||
if (permission.HasFlag(KMemoryPermission.Read))
|
||||
{
|
||||
output = MemoryPermission.Read;
|
||||
}
|
||||
|
||||
if (permission.HasFlag(KMemoryPermission.Write))
|
||||
{
|
||||
output |= MemoryPermission.Write;
|
||||
}
|
||||
|
||||
if (permission.HasFlag(KMemoryPermission.Execute))
|
||||
{
|
||||
output |= MemoryPermission.Execute;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
}
|
@@ -203,15 +203,17 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory
|
||||
/// <inheritdoc/>
|
||||
protected override Result Reprotect(ulong address, ulong pagesCount, KMemoryPermission permission)
|
||||
{
|
||||
// TODO.
|
||||
_cpuMemory.Reprotect(address, pagesCount * PageSize, permission.Convert());
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Result ReprotectWithAttributes(ulong address, ulong pagesCount, KMemoryPermission permission)
|
||||
protected override Result ReprotectAndFlush(ulong address, ulong pagesCount, KMemoryPermission permission)
|
||||
{
|
||||
// TODO.
|
||||
return Result.Success;
|
||||
// TODO: Flush JIT cache.
|
||||
|
||||
return Reprotect(address, pagesCount, permission);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
@@ -1255,7 +1255,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory
|
||||
|
||||
if ((oldPermission & KMemoryPermission.Execute) != 0)
|
||||
{
|
||||
result = ReprotectWithAttributes(address, pagesCount, permission);
|
||||
result = ReprotectAndFlush(address, pagesCount, permission);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -3036,13 +3036,13 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory
|
||||
protected abstract Result Reprotect(ulong address, ulong pagesCount, KMemoryPermission permission);
|
||||
|
||||
/// <summary>
|
||||
/// Changes the permissions of a given virtual memory region.
|
||||
/// Changes the permissions of a given virtual memory region, while also flushing the cache.
|
||||
/// </summary>
|
||||
/// <param name="address">Virtual address of the region to have the permission changes</param>
|
||||
/// <param name="pagesCount">Number of pages to have their permissions changed</param>
|
||||
/// <param name="permission">New permission</param>
|
||||
/// <returns>Result of the permission change operation</returns>
|
||||
protected abstract Result ReprotectWithAttributes(ulong address, ulong pagesCount, KMemoryPermission permission);
|
||||
protected abstract Result ReprotectAndFlush(ulong address, ulong pagesCount, KMemoryPermission permission);
|
||||
|
||||
/// <summary>
|
||||
/// Alerts the memory tracking that a given region has been read from or written to.
|
||||
|
@@ -1,20 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Kernel.Memory
|
||||
{
|
||||
[Flags]
|
||||
enum KMemoryPermission : uint
|
||||
{
|
||||
None = 0,
|
||||
UserMask = Read | Write | Execute,
|
||||
Mask = uint.MaxValue,
|
||||
|
||||
Read = 1 << 0,
|
||||
Write = 1 << 1,
|
||||
Execute = 1 << 2,
|
||||
DontCare = 1 << 28,
|
||||
|
||||
ReadAndWrite = Read | Write,
|
||||
ReadAndExecute = Read | Execute,
|
||||
}
|
||||
}
|
@@ -39,6 +39,8 @@ namespace Ryujinx.HLE.HOS.Services
|
||||
private readonly KernelContext _context;
|
||||
private KProcess _selfProcess;
|
||||
private KThread _selfThread;
|
||||
private KEvent _wakeEvent;
|
||||
private int _wakeHandle = 0;
|
||||
|
||||
private readonly ReaderWriterLockSlim _handleLock = new();
|
||||
private readonly Dictionary<int, IpcService> _sessions = new();
|
||||
@@ -125,6 +127,8 @@ namespace Ryujinx.HLE.HOS.Services
|
||||
_handleLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
_wakeEvent.WritableEvent.Signal();
|
||||
}
|
||||
|
||||
private IpcService GetSessionObj(int serverSessionHandle)
|
||||
@@ -187,6 +191,9 @@ namespace Ryujinx.HLE.HOS.Services
|
||||
AddPort(serverPortHandle, SmObjectFactory);
|
||||
}
|
||||
|
||||
_wakeEvent = new KEvent(_context);
|
||||
Result result = _selfProcess.HandleTable.GenerateHandle(_wakeEvent.ReadableEvent, out _wakeHandle);
|
||||
|
||||
InitDone.Set();
|
||||
|
||||
ulong messagePtr = _selfThread.TlsAddress;
|
||||
@@ -195,7 +202,6 @@ namespace Ryujinx.HLE.HOS.Services
|
||||
_selfProcess.CpuMemory.Write(messagePtr + 0x0, 0);
|
||||
_selfProcess.CpuMemory.Write(messagePtr + 0x4, 2 << 10);
|
||||
_selfProcess.CpuMemory.Write(messagePtr + 0x8, heapAddr | ((ulong)PointerBufferSize << 48));
|
||||
|
||||
int replyTargetHandle = 0;
|
||||
|
||||
while (true)
|
||||
@@ -211,13 +217,15 @@ namespace Ryujinx.HLE.HOS.Services
|
||||
|
||||
portHandleCount = _ports.Count;
|
||||
|
||||
handleCount = portHandleCount + _sessions.Count;
|
||||
handleCount = portHandleCount + _sessions.Count + 1;
|
||||
|
||||
handles = ArrayPool<int>.Shared.Rent(handleCount);
|
||||
|
||||
_ports.Keys.CopyTo(handles, 0);
|
||||
handles[0] = _wakeHandle;
|
||||
|
||||
_sessions.Keys.CopyTo(handles, portHandleCount);
|
||||
_ports.Keys.CopyTo(handles, 1);
|
||||
|
||||
_sessions.Keys.CopyTo(handles, portHandleCount + 1);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -227,8 +235,7 @@ namespace Ryujinx.HLE.HOS.Services
|
||||
}
|
||||
}
|
||||
|
||||
// We still need a timeout here to allow the service to pick up and listen new sessions...
|
||||
var rc = _context.Syscall.ReplyAndReceive(out int signaledIndex, handles.AsSpan(0, handleCount), replyTargetHandle, 1000000L);
|
||||
var rc = _context.Syscall.ReplyAndReceive(out int signaledIndex, handles.AsSpan(0, handleCount), replyTargetHandle, -1);
|
||||
|
||||
_selfThread.HandlePostSyscall();
|
||||
|
||||
@@ -239,7 +246,7 @@ namespace Ryujinx.HLE.HOS.Services
|
||||
|
||||
replyTargetHandle = 0;
|
||||
|
||||
if (rc == Result.Success && signaledIndex >= portHandleCount)
|
||||
if (rc == Result.Success && signaledIndex >= portHandleCount + 1)
|
||||
{
|
||||
// We got a IPC request, process it, pass to the appropriate service if needed.
|
||||
int signaledHandle = handles[signaledIndex];
|
||||
@@ -253,24 +260,32 @@ namespace Ryujinx.HLE.HOS.Services
|
||||
{
|
||||
if (rc == Result.Success)
|
||||
{
|
||||
// We got a new connection, accept the session to allow servicing future requests.
|
||||
if (_context.Syscall.AcceptSession(out int serverSessionHandle, handles[signaledIndex]) == Result.Success)
|
||||
if (signaledIndex > 0)
|
||||
{
|
||||
bool handleWriteLockTaken = false;
|
||||
try
|
||||
// We got a new connection, accept the session to allow servicing future requests.
|
||||
if (_context.Syscall.AcceptSession(out int serverSessionHandle, handles[signaledIndex]) == Result.Success)
|
||||
{
|
||||
handleWriteLockTaken = _handleLock.TryEnterWriteLock(Timeout.Infinite);
|
||||
IpcService obj = _ports[handles[signaledIndex]].Invoke();
|
||||
_sessions.Add(serverSessionHandle, obj);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (handleWriteLockTaken)
|
||||
bool handleWriteLockTaken = false;
|
||||
try
|
||||
{
|
||||
_handleLock.ExitWriteLock();
|
||||
handleWriteLockTaken = _handleLock.TryEnterWriteLock(Timeout.Infinite);
|
||||
IpcService obj = _ports[handles[signaledIndex]].Invoke();
|
||||
_sessions.Add(serverSessionHandle, obj);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (handleWriteLockTaken)
|
||||
{
|
||||
_handleLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// The _wakeEvent signalled, which means we have a new session.
|
||||
_wakeEvent.WritableEvent.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
_selfProcess.CpuMemory.Write(messagePtr + 0x0, 0);
|
||||
@@ -499,6 +514,8 @@ namespace Ryujinx.HLE.HOS.Services
|
||||
|
||||
if (Interlocked.Exchange(ref _isDisposed, 1) == 0)
|
||||
{
|
||||
_selfProcess.HandleTable.CloseHandle(_wakeHandle);
|
||||
|
||||
foreach (IpcService service in _sessions.Values)
|
||||
{
|
||||
(service as IDisposable)?.Dispose();
|
||||
|
@@ -455,6 +455,11 @@ namespace Ryujinx.Memory
|
||||
return _pageTable.Read(va) + (nuint)(va & PageMask);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Reprotect(ulong va, ulong size, MemoryPermission protection)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
|
||||
{
|
||||
|
@@ -104,6 +104,12 @@ namespace Ryujinx.Memory
|
||||
/// <returns>True if the data was changed, false otherwise</returns>
|
||||
bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan<byte> data);
|
||||
|
||||
/// <summary>
|
||||
/// Fills the specified memory region with the value specified in <paramref name="value"/>.
|
||||
/// </summary>
|
||||
/// <param name="va">Virtual address to fill the value into</param>
|
||||
/// <param name="size">Size of the memory region to fill</param>
|
||||
/// <param name="value">Value to fill with</param>
|
||||
void Fill(ulong va, ulong size, byte value)
|
||||
{
|
||||
const int MaxChunkSize = 1 << 24;
|
||||
@@ -194,6 +200,14 @@ namespace Ryujinx.Memory
|
||||
/// <param name="exemptId">Optional ID of the handles that should not be signalled</param>
|
||||
void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Reprotect a region of virtual memory for guest access.
|
||||
/// </summary>
|
||||
/// <param name="va">Virtual address base</param>
|
||||
/// <param name="size">Size of the region to protect</param>
|
||||
/// <param name="protection">Memory protection to set</param>
|
||||
void Reprotect(ulong va, ulong size, MemoryPermission protection);
|
||||
|
||||
/// <summary>
|
||||
/// Reprotect a region of virtual memory for tracking.
|
||||
/// </summary>
|
||||
|
@@ -102,6 +102,11 @@ namespace Ryujinx.Tests.Memory
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Reprotect(ulong va, ulong size, MemoryPermission protection)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
|
||||
{
|
||||
OnProtect?.Invoke(va, size, protection);
|
||||
|
@@ -9,10 +9,9 @@ using LibHac.Tools.FsSystem;
|
||||
using LibHac.Tools.FsSystem.NcaUtils;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Ui.App.Common
|
||||
{
|
||||
@@ -24,29 +23,18 @@ namespace Ryujinx.Ui.App.Common
|
||||
public string TitleId { get; set; }
|
||||
public string Developer { get; set; }
|
||||
public string Version { get; set; }
|
||||
public string TimePlayed { get; set; }
|
||||
public double TimePlayedNum { get; set; }
|
||||
public TimeSpan TimePlayed { get; set; }
|
||||
public DateTime? LastPlayed { get; set; }
|
||||
public string FileExtension { get; set; }
|
||||
public string FileSize { get; set; }
|
||||
public double FileSizeBytes { get; set; }
|
||||
public long FileSize { get; set; }
|
||||
public string Path { get; set; }
|
||||
public BlitStruct<ApplicationControlProperty> ControlHolder { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string LastPlayedString
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!LastPlayed.HasValue)
|
||||
{
|
||||
// TODO: maybe put localized string here instead of just "Never"
|
||||
return "Never";
|
||||
}
|
||||
public string TimePlayedString => ValueFormatUtils.FormatTimeSpan(TimePlayed);
|
||||
|
||||
return LastPlayed.Value.ToLocalTime().ToString(CultureInfo.CurrentCulture);
|
||||
}
|
||||
}
|
||||
public string LastPlayedString => ValueFormatUtils.FormatDateTime(LastPlayed);
|
||||
|
||||
public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize);
|
||||
|
||||
public static string GetApplicationBuildId(VirtualFileSystem virtualFileSystem, string titleFilePath)
|
||||
{
|
||||
|
@@ -155,7 +155,7 @@ namespace Ryujinx.Ui.App.Common
|
||||
return;
|
||||
}
|
||||
|
||||
double fileSize = new FileInfo(applicationPath).Length * 0.000000000931;
|
||||
long fileSize = new FileInfo(applicationPath).Length;
|
||||
string titleName = "Unknown";
|
||||
string titleId = "0000000000000000";
|
||||
string developer = "Unknown";
|
||||
@@ -425,25 +425,25 @@ namespace Ryujinx.Ui.App.Common
|
||||
{
|
||||
appMetadata.Title = titleName;
|
||||
|
||||
if (appMetadata.LastPlayedOld == default || appMetadata.LastPlayed.HasValue)
|
||||
// Only do the migration if time_played has a value and timespan_played hasn't been updated yet.
|
||||
if (appMetadata.TimePlayedOld != default && appMetadata.TimePlayed == TimeSpan.Zero)
|
||||
{
|
||||
// Don't do the migration if last_played doesn't exist or last_played_utc already has a value.
|
||||
return;
|
||||
appMetadata.TimePlayed = TimeSpan.FromSeconds(appMetadata.TimePlayedOld);
|
||||
appMetadata.TimePlayedOld = default;
|
||||
}
|
||||
|
||||
// Migrate from string-based last_played to DateTime-based last_played_utc.
|
||||
if (DateTime.TryParse(appMetadata.LastPlayedOld, out DateTime lastPlayedOldParsed))
|
||||
// Only do the migration if last_played has a value and last_played_utc doesn't exist yet.
|
||||
if (appMetadata.LastPlayedOld != default && !appMetadata.LastPlayed.HasValue)
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Application, $"last_played found: \"{appMetadata.LastPlayedOld}\", migrating to last_played_utc");
|
||||
appMetadata.LastPlayed = lastPlayedOldParsed;
|
||||
// Migrate from string-based last_played to DateTime-based last_played_utc.
|
||||
if (DateTime.TryParse(appMetadata.LastPlayedOld, out DateTime lastPlayedOldParsed))
|
||||
{
|
||||
appMetadata.LastPlayed = lastPlayedOldParsed;
|
||||
|
||||
// Migration successful: deleting last_played from the metadata file.
|
||||
appMetadata.LastPlayedOld = default;
|
||||
}
|
||||
|
||||
// Migration successful: deleting last_played from the metadata file.
|
||||
appMetadata.LastPlayedOld = default;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Migration failed: emitting warning but leaving the unparsable value in the metadata file so the user can fix it.
|
||||
Logger.Warning?.Print(LogClass.Application, $"Last played string \"{appMetadata.LastPlayedOld}\" is invalid for current system culture, skipping (did current culture change?)");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -455,12 +455,10 @@ namespace Ryujinx.Ui.App.Common
|
||||
TitleId = titleId,
|
||||
Developer = developer,
|
||||
Version = version,
|
||||
TimePlayed = ConvertSecondsToFormattedString(appMetadata.TimePlayed),
|
||||
TimePlayedNum = appMetadata.TimePlayed,
|
||||
TimePlayed = appMetadata.TimePlayed,
|
||||
LastPlayed = appMetadata.LastPlayed,
|
||||
FileExtension = Path.GetExtension(applicationPath).ToUpper().Remove(0, 1),
|
||||
FileSize = (fileSize < 1) ? (fileSize * 1024).ToString("0.##") + " MiB" : fileSize.ToString("0.##") + " GiB",
|
||||
FileSizeBytes = fileSize,
|
||||
FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper(),
|
||||
FileSize = fileSize,
|
||||
Path = applicationPath,
|
||||
ControlHolder = controlHolder,
|
||||
};
|
||||
@@ -716,31 +714,6 @@ namespace Ryujinx.Ui.App.Common
|
||||
return applicationIcon ?? _ncaIcon;
|
||||
}
|
||||
|
||||
private static string ConvertSecondsToFormattedString(double seconds)
|
||||
{
|
||||
TimeSpan time = TimeSpan.FromSeconds(seconds);
|
||||
|
||||
string timeString;
|
||||
if (time.Days != 0)
|
||||
{
|
||||
timeString = $"{time.Days}d {time.Hours:D2}h {time.Minutes:D2}m";
|
||||
}
|
||||
else if (time.Hours != 0)
|
||||
{
|
||||
timeString = $"{time.Hours:D2}h {time.Minutes:D2}m";
|
||||
}
|
||||
else if (time.Minutes != 0)
|
||||
{
|
||||
timeString = $"{time.Minutes:D2}m";
|
||||
}
|
||||
else
|
||||
{
|
||||
timeString = "Never";
|
||||
}
|
||||
|
||||
return timeString;
|
||||
}
|
||||
|
||||
private void GetGameInformation(ref ApplicationControlProperty controlData, out string titleName, out string titleId, out string publisher, out string version)
|
||||
{
|
||||
_ = Enum.TryParse(_desiredTitleLanguage.ToString(), out TitleLanguage desiredTitleLanguage);
|
||||
|
@@ -7,13 +7,45 @@ namespace Ryujinx.Ui.App.Common
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public bool Favorite { get; set; }
|
||||
public double TimePlayed { get; set; }
|
||||
|
||||
[JsonPropertyName("timespan_played")]
|
||||
public TimeSpan TimePlayed { get; set; } = TimeSpan.Zero;
|
||||
|
||||
[JsonPropertyName("last_played_utc")]
|
||||
public DateTime? LastPlayed { get; set; } = null;
|
||||
|
||||
[JsonPropertyName("time_played")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||
public double TimePlayedOld { get; set; }
|
||||
|
||||
[JsonPropertyName("last_played")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||
public string LastPlayedOld { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Updates <see cref="LastPlayed"/>. Call this before launching a game.
|
||||
/// </summary>
|
||||
public void UpdatePreGame()
|
||||
{
|
||||
LastPlayed = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates <see cref="LastPlayed"/> and <see cref="TimePlayed"/>. Call this after a game ends.
|
||||
/// </summary>
|
||||
public void UpdatePostGame()
|
||||
{
|
||||
DateTime? prevLastPlayed = LastPlayed;
|
||||
UpdatePreGame();
|
||||
|
||||
if (!prevLastPlayed.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TimeSpan diff = DateTime.UtcNow - prevLastPlayed.Value;
|
||||
double newTotalSeconds = TimePlayed.Add(diff).TotalSeconds;
|
||||
TimePlayed = TimeSpan.FromSeconds(Math.Round(newTotalSeconds, MidpointRounding.AwayFromZero));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
219
src/Ryujinx.Ui.Common/Helper/ValueFormatUtils.cs
Normal file
219
src/Ryujinx.Ui.Common/Helper/ValueFormatUtils.cs
Normal file
@@ -0,0 +1,219 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ryujinx.Ui.Common.Helper
|
||||
{
|
||||
public static class ValueFormatUtils
|
||||
{
|
||||
private static readonly string[] _fileSizeUnitStrings =
|
||||
{
|
||||
"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", // Base 10 units, used for formatting and parsing
|
||||
"KB", "MB", "GB", "TB", "PB", "EB", // Base 2 units, used for parsing legacy values
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Used by <see cref="FormatFileSize"/>.
|
||||
/// </summary>
|
||||
public enum FileSizeUnits
|
||||
{
|
||||
Auto = -1,
|
||||
Bytes = 0,
|
||||
Kibibytes = 1,
|
||||
Mebibytes = 2,
|
||||
Gibibytes = 3,
|
||||
Tebibytes = 4,
|
||||
Pebibytes = 5,
|
||||
Exbibytes = 6,
|
||||
Kilobytes = 7,
|
||||
Megabytes = 8,
|
||||
Gigabytes = 9,
|
||||
Terabytes = 10,
|
||||
Petabytes = 11,
|
||||
Exabytes = 12,
|
||||
}
|
||||
|
||||
private const double SizeBase10 = 1000;
|
||||
private const double SizeBase2 = 1024;
|
||||
private const int UnitEBIndex = 6;
|
||||
|
||||
#region Value formatters
|
||||
|
||||
/// <summary>
|
||||
/// Creates a human-readable string from a <see cref="TimeSpan"/>.
|
||||
/// </summary>
|
||||
/// <param name="timeSpan">The <see cref="TimeSpan"/> to be formatted.</param>
|
||||
/// <returns>A formatted string that can be displayed in the UI.</returns>
|
||||
public static string FormatTimeSpan(TimeSpan? timeSpan)
|
||||
{
|
||||
if (!timeSpan.HasValue || timeSpan.Value.TotalSeconds < 1)
|
||||
{
|
||||
// Game was never played
|
||||
return TimeSpan.Zero.ToString("c", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (timeSpan.Value.TotalDays < 1)
|
||||
{
|
||||
// Game was played for less than a day
|
||||
return timeSpan.Value.ToString("c", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
// Game was played for more than a day
|
||||
TimeSpan onlyTime = timeSpan.Value.Subtract(TimeSpan.FromDays(timeSpan.Value.Days));
|
||||
string onlyTimeString = onlyTime.ToString("c", CultureInfo.InvariantCulture);
|
||||
|
||||
return $"{timeSpan.Value.Days}d, {onlyTimeString}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a human-readable string from a <see cref="DateTime"/>.
|
||||
/// </summary>
|
||||
/// <param name="utcDateTime">The <see cref="DateTime"/> to be formatted. This is expected to be UTC-based.</param>
|
||||
/// <param name="culture">The <see cref="CultureInfo"/> that's used in formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.</param>
|
||||
/// <returns>A formatted string that can be displayed in the UI.</returns>
|
||||
public static string FormatDateTime(DateTime? utcDateTime, CultureInfo culture = null)
|
||||
{
|
||||
culture ??= CultureInfo.CurrentCulture;
|
||||
|
||||
if (!utcDateTime.HasValue)
|
||||
{
|
||||
// In the Avalonia UI, this is turned into a localized version of "Never" by LocalizedNeverConverter.
|
||||
return "Never";
|
||||
}
|
||||
|
||||
return utcDateTime.Value.ToLocalTime().ToString(culture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a human-readable file size string.
|
||||
/// </summary>
|
||||
/// <param name="size">The file size in bytes.</param>
|
||||
/// <param name="forceUnit">Formats the passed size value as this unit, bypassing the automatic unit choice.</param>
|
||||
/// <returns>A human-readable file size string.</returns>
|
||||
public static string FormatFileSize(long size, FileSizeUnits forceUnit = FileSizeUnits.Auto)
|
||||
{
|
||||
if (size <= 0)
|
||||
{
|
||||
return $"0 {_fileSizeUnitStrings[0]}";
|
||||
}
|
||||
|
||||
int unitIndex = (int)forceUnit;
|
||||
if (forceUnit == FileSizeUnits.Auto)
|
||||
{
|
||||
unitIndex = Convert.ToInt32(Math.Floor(Math.Log(size, SizeBase10)));
|
||||
|
||||
// Apply an upper bound so that exabytes are the biggest unit used when formatting.
|
||||
if (unitIndex > UnitEBIndex)
|
||||
{
|
||||
unitIndex = UnitEBIndex;
|
||||
}
|
||||
}
|
||||
|
||||
double sizeRounded;
|
||||
|
||||
if (unitIndex > UnitEBIndex)
|
||||
{
|
||||
sizeRounded = Math.Round(size / Math.Pow(SizeBase10, unitIndex - UnitEBIndex), 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
sizeRounded = Math.Round(size / Math.Pow(SizeBase2, unitIndex), 1);
|
||||
}
|
||||
|
||||
string sizeFormatted = sizeRounded.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
return $"{sizeFormatted} {_fileSizeUnitStrings[unitIndex]}";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Value parsers
|
||||
|
||||
/// <summary>
|
||||
/// Parses a string generated by <see cref="FormatTimeSpan"/> and returns the original <see cref="TimeSpan"/>.
|
||||
/// </summary>
|
||||
/// <param name="timeSpanString">A string representing a <see cref="TimeSpan"/>.</param>
|
||||
/// <returns>A <see cref="TimeSpan"/> object. If the input string couldn't been parsed, <see cref="TimeSpan.Zero"/> is returned.</returns>
|
||||
public static TimeSpan ParseTimeSpan(string timeSpanString)
|
||||
{
|
||||
TimeSpan returnTimeSpan = TimeSpan.Zero;
|
||||
|
||||
// An input string can either look like "01:23:45" or "1d, 01:23:45" if the timespan represents a duration of more than a day.
|
||||
// Here, we split the input string to check if it's the former or the latter.
|
||||
var valueSplit = timeSpanString.Split(", ");
|
||||
if (valueSplit.Length > 1)
|
||||
{
|
||||
var dayPart = valueSplit[0].Split("d")[0];
|
||||
if (int.TryParse(dayPart, out int days))
|
||||
{
|
||||
returnTimeSpan = returnTimeSpan.Add(TimeSpan.FromDays(days));
|
||||
}
|
||||
}
|
||||
|
||||
if (TimeSpan.TryParse(valueSplit.Last(), out TimeSpan parsedTimeSpan))
|
||||
{
|
||||
returnTimeSpan = returnTimeSpan.Add(parsedTimeSpan);
|
||||
}
|
||||
|
||||
return returnTimeSpan;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a string generated by <see cref="FormatDateTime"/> and returns the original <see cref="DateTime"/>.
|
||||
/// </summary>
|
||||
/// <param name="dateTimeString">The string representing a <see cref="DateTime"/>.</param>
|
||||
/// <returns>A <see cref="DateTime"/> object. If the input string couldn't be parsed, <see cref="DateTime.UnixEpoch"/> is returned.</returns>
|
||||
public static DateTime ParseDateTime(string dateTimeString)
|
||||
{
|
||||
if (!DateTime.TryParse(dateTimeString, CultureInfo.CurrentCulture, out DateTime parsedDateTime))
|
||||
{
|
||||
// Games that were never played are supposed to appear before the oldest played games in the list,
|
||||
// so returning DateTime.UnixEpoch here makes sense.
|
||||
return DateTime.UnixEpoch;
|
||||
}
|
||||
|
||||
return parsedDateTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a string generated by <see cref="FormatFileSize"/> and returns a <see cref="long"/> representing a number of bytes.
|
||||
/// </summary>
|
||||
/// <param name="sizeString">A string representing a file size formatted with <see cref="FormatFileSize"/>.</param>
|
||||
/// <returns>A <see cref="long"/> representing a number of bytes.</returns>
|
||||
public static long ParseFileSize(string sizeString)
|
||||
{
|
||||
// Enumerating over the units backwards because otherwise, sizeString.EndsWith("B") would exit the loop in the first iteration.
|
||||
for (int i = _fileSizeUnitStrings.Length - 1; i >= 0; i--)
|
||||
{
|
||||
string unit = _fileSizeUnitStrings[i];
|
||||
if (!sizeString.EndsWith(unit))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string numberString = sizeString.Split(" ")[0];
|
||||
if (!double.TryParse(numberString, CultureInfo.InvariantCulture, out double number))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
double sizeBase = SizeBase2;
|
||||
|
||||
// If the unit index is one that points to a base 10 unit in the FileSizeUnitStrings array, subtract 6 to arrive at a usable power value.
|
||||
if (i > UnitEBIndex)
|
||||
{
|
||||
i -= UnitEBIndex;
|
||||
sizeBase = SizeBase10;
|
||||
}
|
||||
|
||||
number *= Math.Pow(sizeBase, i);
|
||||
|
||||
return Convert.ToInt64(number);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
@@ -5,7 +5,7 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace Ryujinx.Common.SystemInfo
|
||||
namespace Ryujinx.Ui.Common.SystemInfo
|
||||
{
|
||||
[SupportedOSPlatform("linux")]
|
||||
class LinuxSystemInfo : SystemInfo
|
@@ -5,7 +5,7 @@ using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
|
||||
namespace Ryujinx.Common.SystemInfo
|
||||
namespace Ryujinx.Ui.Common.SystemInfo
|
||||
{
|
||||
[SupportedOSPlatform("macos")]
|
||||
partial class MacOSSystemInfo : SystemInfo
|
@@ -1,10 +1,11 @@
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Intrinsics.X86;
|
||||
using System.Text;
|
||||
|
||||
namespace Ryujinx.Common.SystemInfo
|
||||
namespace Ryujinx.Ui.Common.SystemInfo
|
||||
{
|
||||
public class SystemInfo
|
||||
{
|
||||
@@ -20,13 +21,13 @@ namespace Ryujinx.Common.SystemInfo
|
||||
CpuName = "Unknown";
|
||||
}
|
||||
|
||||
private static string ToMiBString(ulong bytesValue) => (bytesValue == 0) ? "Unknown" : $"{bytesValue / 1024 / 1024} MiB";
|
||||
private static string ToGBString(ulong bytesValue) => (bytesValue == 0) ? "Unknown" : ValueFormatUtils.FormatFileSize((long)bytesValue, ValueFormatUtils.FileSizeUnits.Gibibytes);
|
||||
|
||||
public void Print()
|
||||
{
|
||||
Logger.Notice.Print(LogClass.Application, $"Operating System: {OsDescription}");
|
||||
Logger.Notice.Print(LogClass.Application, $"CPU: {CpuName}");
|
||||
Logger.Notice.Print(LogClass.Application, $"RAM: Total {ToMiBString(RamTotal)} ; Available {ToMiBString(RamAvailable)}");
|
||||
Logger.Notice.Print(LogClass.Application, $"RAM: Total {ToGBString(RamTotal)} ; Available {ToGBString(RamAvailable)}");
|
||||
}
|
||||
|
||||
public static SystemInfo Gather()
|
@@ -4,7 +4,7 @@ using System.Management;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace Ryujinx.Common.SystemInfo
|
||||
namespace Ryujinx.Ui.Common.SystemInfo
|
||||
{
|
||||
[SupportedOSPlatform("windows")]
|
||||
partial class WindowsSystemInfo : SystemInfo
|
@@ -3,7 +3,6 @@ using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.GraphicsDriver;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.SystemInfo;
|
||||
using Ryujinx.Common.SystemInterop;
|
||||
using Ryujinx.Modules;
|
||||
using Ryujinx.SDL2.Common;
|
||||
@@ -11,6 +10,7 @@ using Ryujinx.Ui;
|
||||
using Ryujinx.Ui.Common;
|
||||
using Ryujinx.Ui.Common.Configuration;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using Ryujinx.Ui.Common.SystemInfo;
|
||||
using Ryujinx.Ui.Widgets;
|
||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||
using System;
|
||||
|
@@ -1,4 +1,5 @@
|
||||
using Gtk;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Ui.Helper
|
||||
@@ -7,88 +8,26 @@ namespace Ryujinx.Ui.Helper
|
||||
{
|
||||
public static int TimePlayedSort(ITreeModel model, TreeIter a, TreeIter b)
|
||||
{
|
||||
static string ReverseFormat(string time)
|
||||
{
|
||||
if (time == "Never")
|
||||
{
|
||||
return "00";
|
||||
}
|
||||
TimeSpan aTimeSpan = ValueFormatUtils.ParseTimeSpan(model.GetValue(a, 5).ToString());
|
||||
TimeSpan bTimeSpan = ValueFormatUtils.ParseTimeSpan(model.GetValue(b, 5).ToString());
|
||||
|
||||
var numbers = time.Split(new char[] { 'd', 'h', 'm' });
|
||||
|
||||
time = time.Replace(" ", "").Replace("d", ".").Replace("h", ":").Replace("m", "");
|
||||
|
||||
if (numbers.Length == 2)
|
||||
{
|
||||
return $"00.00:{time}";
|
||||
}
|
||||
else if (numbers.Length == 3)
|
||||
{
|
||||
return $"00.{time}";
|
||||
}
|
||||
|
||||
return time;
|
||||
}
|
||||
|
||||
string aValue = ReverseFormat(model.GetValue(a, 5).ToString());
|
||||
string bValue = ReverseFormat(model.GetValue(b, 5).ToString());
|
||||
|
||||
return TimeSpan.Compare(TimeSpan.Parse(aValue), TimeSpan.Parse(bValue));
|
||||
return TimeSpan.Compare(aTimeSpan, bTimeSpan);
|
||||
}
|
||||
|
||||
public static int LastPlayedSort(ITreeModel model, TreeIter a, TreeIter b)
|
||||
{
|
||||
string aValue = model.GetValue(a, 6).ToString();
|
||||
string bValue = model.GetValue(b, 6).ToString();
|
||||
DateTime aDateTime = ValueFormatUtils.ParseDateTime(model.GetValue(a, 6).ToString());
|
||||
DateTime bDateTime = ValueFormatUtils.ParseDateTime(model.GetValue(b, 6).ToString());
|
||||
|
||||
if (aValue == "Never")
|
||||
{
|
||||
aValue = DateTime.UnixEpoch.ToString();
|
||||
}
|
||||
|
||||
if (bValue == "Never")
|
||||
{
|
||||
bValue = DateTime.UnixEpoch.ToString();
|
||||
}
|
||||
|
||||
return DateTime.Compare(DateTime.Parse(bValue), DateTime.Parse(aValue));
|
||||
return DateTime.Compare(aDateTime, bDateTime);
|
||||
}
|
||||
|
||||
public static int FileSizeSort(ITreeModel model, TreeIter a, TreeIter b)
|
||||
{
|
||||
string aValue = model.GetValue(a, 8).ToString();
|
||||
string bValue = model.GetValue(b, 8).ToString();
|
||||
long aSize = ValueFormatUtils.ParseFileSize(model.GetValue(a, 8).ToString());
|
||||
long bSize = ValueFormatUtils.ParseFileSize(model.GetValue(b, 8).ToString());
|
||||
|
||||
if (aValue[^3..] == "GiB")
|
||||
{
|
||||
aValue = (float.Parse(aValue[0..^3]) * 1024).ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
aValue = aValue[0..^3];
|
||||
}
|
||||
|
||||
if (bValue[^3..] == "GiB")
|
||||
{
|
||||
bValue = (float.Parse(bValue[0..^3]) * 1024).ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
bValue = bValue[0..^3];
|
||||
}
|
||||
|
||||
if (float.Parse(aValue) > float.Parse(bValue))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
else if (float.Parse(bValue) > float.Parse(aValue))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return aSize.CompareTo(bSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -954,7 +954,7 @@ namespace Ryujinx.Ui
|
||||
|
||||
ApplicationLibrary.LoadAndSaveMetaData(_emulationContext.Processes.ActiveApplication.ProgramIdText, appMetadata =>
|
||||
{
|
||||
appMetadata.LastPlayed = DateTime.UtcNow;
|
||||
appMetadata.UpdatePreGame();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1097,13 +1097,7 @@ namespace Ryujinx.Ui
|
||||
{
|
||||
ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata =>
|
||||
{
|
||||
if (appMetadata.LastPlayed.HasValue)
|
||||
{
|
||||
double sessionTimePlayed = DateTime.UtcNow.Subtract(appMetadata.LastPlayed.Value).TotalSeconds;
|
||||
appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
appMetadata.LastPlayed = DateTime.UtcNow;
|
||||
appMetadata.UpdatePostGame();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1177,10 +1171,10 @@ namespace Ryujinx.Ui
|
||||
$"{args.AppData.TitleName}\n{args.AppData.TitleId.ToUpper()}",
|
||||
args.AppData.Developer,
|
||||
args.AppData.Version,
|
||||
args.AppData.TimePlayed,
|
||||
args.AppData.TimePlayedString,
|
||||
args.AppData.LastPlayedString,
|
||||
args.AppData.FileExtension,
|
||||
args.AppData.FileSize,
|
||||
args.AppData.FileSizeString,
|
||||
args.AppData.Path,
|
||||
args.AppData.ControlHolder);
|
||||
});
|
||||
|
Reference in New Issue
Block a user