Compare commits

..

10 Commits

Author SHA1 Message Date
FluffyOMC
96dd48fa40 Merge 753ca01c0d into 566f3d079a 2025-02-04 11:31:57 +04:00
FluffyOMC
753ca01c0d Merge branch 'master' into master 2025-02-03 23:26:34 -05:00
FluffyOMC
5aedeebfe9 Merge branch 'master' into master 2025-02-03 20:32:25 -05:00
VocalFan
5c67efd291 MacOS gets it too :3 2025-02-03 03:55:38 -05:00
VocalFan
b2354768c4 Change Region size from 256MB to 128MB 2025-02-03 03:17:22 -05:00
FluffyOMC
745bd91250 Merge branch 'master' into master 2025-02-03 02:37:46 -05:00
VocalFan
07ab817557 Merge branch 'master' of https://github.com/FluffyOMC/Ryujinx-Bingus 2025-02-03 02:29:59 -05:00
VocalFan
0e8a41b198 Humanizer boi 2025-02-03 02:29:44 -05:00
FluffyOMC
969e94f913 Merge branch 'Ryubing:master' into master 2025-02-03 02:06:17 -05:00
VocalFan
2e2d26d49d Cursed thing that fixes JIT Cache exhaustion 2025-02-03 02:05:40 -05:00
21 changed files with 182 additions and 499 deletions

View File

@@ -7,6 +7,7 @@ namespace ARMeilleure.Memory
public const int DefaultGranularity = 65536; // Mapping granularity in Windows.
public IJitMemoryBlock Block { get; }
public IJitMemoryAllocator Allocator { get; }
public nint Pointer => Block.Pointer;
@@ -21,6 +22,7 @@ namespace ARMeilleure.Memory
granularity = DefaultGranularity;
}
Allocator = allocator;
Block = allocator.Reserve(maxSize);
_maxSize = maxSize;
_sizeGranularity = granularity;

View File

@@ -2,6 +2,8 @@ using ARMeilleure.CodeGen;
using ARMeilleure.CodeGen.Unwinding;
using ARMeilleure.Memory;
using ARMeilleure.Native;
using Humanizer;
using Ryujinx.Common.Logging;
using Ryujinx.Memory;
using System;
using System.Collections.Generic;
@@ -18,9 +20,8 @@ namespace ARMeilleure.Translation.Cache
private static readonly int _pageMask = _pageSize - 1;
private const int CodeAlignment = 4; // Bytes.
private const int CacheSize = 2047 * 1024 * 1024;
private const int CacheSize = 128 * 1024 * 1024;
private static ReservedRegion _jitRegion;
private static JitCacheInvalidation _jitCacheInvalidator;
private static CacheMemoryAllocator _cacheAllocator;
@@ -30,6 +31,9 @@ namespace ARMeilleure.Translation.Cache
private static readonly Lock _lock = new();
private static bool _initialized;
private static readonly List<ReservedRegion> _jitRegions = new();
private static int _activeRegionIndex = 0;
[SupportedOSPlatform("windows")]
[LibraryImport("kernel32.dll", SetLastError = true)]
public static partial nint FlushInstructionCache(nint hProcess, nint lpAddress, nuint dwSize);
@@ -48,7 +52,9 @@ namespace ARMeilleure.Translation.Cache
return;
}
_jitRegion = new ReservedRegion(allocator, CacheSize);
var firstRegion = new ReservedRegion(allocator, CacheSize);
_jitRegions.Add(firstRegion);
_activeRegionIndex = 0;
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
{
@@ -59,7 +65,9 @@ namespace ARMeilleure.Translation.Cache
if (OperatingSystem.IsWindows())
{
JitUnwindWindows.InstallFunctionTableHandler(_jitRegion.Pointer, CacheSize, _jitRegion.Pointer + Allocate(_pageSize));
JitUnwindWindows.InstallFunctionTableHandler(
firstRegion.Pointer, CacheSize, firstRegion.Pointer + Allocate(_pageSize)
);
}
_initialized = true;
@@ -75,8 +83,8 @@ namespace ARMeilleure.Translation.Cache
Debug.Assert(_initialized);
int funcOffset = Allocate(code.Length);
nint funcPtr = _jitRegion.Pointer + funcOffset;
ReservedRegion targetRegion = _jitRegions[_activeRegionIndex];
nint funcPtr = targetRegion.Pointer + funcOffset;
if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{
@@ -90,9 +98,9 @@ namespace ARMeilleure.Translation.Cache
}
else
{
ReprotectAsWritable(funcOffset, code.Length);
ReprotectAsWritable(targetRegion, funcOffset, code.Length);
Marshal.Copy(code, 0, funcPtr, code.Length);
ReprotectAsExecutable(funcOffset, code.Length);
ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{
@@ -116,52 +124,83 @@ namespace ARMeilleure.Translation.Cache
{
Debug.Assert(_initialized);
int funcOffset = (int)(pointer.ToInt64() - _jitRegion.Pointer.ToInt64());
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
foreach (var region in _jitRegions)
{
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
_cacheEntries.RemoveAt(entryIndex);
if (pointer.ToInt64() < region.Pointer.ToInt64() ||
pointer.ToInt64() >= (region.Pointer + CacheSize).ToInt64())
{
continue;
}
int funcOffset = (int)(pointer.ToInt64() - region.Pointer.ToInt64());
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
{
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
_cacheEntries.RemoveAt(entryIndex);
}
return;
}
}
}
private static void ReprotectAsWritable(int offset, int size)
private static void ReprotectAsWritable(ReservedRegion region, int offset, int size)
{
int endOffs = offset + size;
int regionStart = offset & ~_pageMask;
int regionEnd = (endOffs + _pageMask) & ~_pageMask;
_jitRegion.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
region.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
}
private static void ReprotectAsExecutable(int offset, int size)
private static void ReprotectAsExecutable(ReservedRegion region, int offset, int size)
{
int endOffs = offset + size;
int regionStart = offset & ~_pageMask;
int regionEnd = (endOffs + _pageMask) & ~_pageMask;
_jitRegion.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
region.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
}
private static int Allocate(int codeSize)
{
codeSize = AlignCodeSize(codeSize);
int allocOffset = _cacheAllocator.Allocate(codeSize);
if (allocOffset < 0)
for (int i = _activeRegionIndex; i < _jitRegions.Count; i++)
{
throw new OutOfMemoryException("JIT Cache exhausted.");
int allocOffset = _cacheAllocator.Allocate(codeSize);
if (allocOffset >= 0)
{
_jitRegions[i].ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
_activeRegionIndex = i;
return allocOffset;
}
}
_jitRegion.ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
int exhaustedRegion = _activeRegionIndex;
var newRegion = new ReservedRegion(_jitRegions[0].Allocator, CacheSize);
_jitRegions.Add(newRegion);
_activeRegionIndex = _jitRegions.Count - 1;
int newRegionNumber = _activeRegionIndex;
return allocOffset;
Logger.Warning?.Print(LogClass.Cpu, $"JIT Cache Region {exhaustedRegion} exhausted, creating new Cache Region {newRegionNumber} ({((newRegionNumber + 1) * CacheSize).Bytes()} Total Allocation).");
_cacheAllocator = new CacheMemoryAllocator(CacheSize);
int allocOffsetNew = _cacheAllocator.Allocate(codeSize);
if (allocOffsetNew < 0)
{
throw new OutOfMemoryException("Failed to allocate in new Cache Region!");
}
newRegion.ExpandIfNeeded((ulong)allocOffsetNew + (ulong)codeSize);
return allocOffsetNew;
}
private static int AlignCodeSize(int codeSize)
{
return checked(codeSize + (CodeAlignment - 1)) & ~(CodeAlignment - 1);
@@ -185,18 +224,21 @@ namespace ARMeilleure.Translation.Cache
{
lock (_lock)
{
int index = _cacheEntries.BinarySearch(new CacheEntry(offset, 0, default));
if (index < 0)
foreach (var region in _jitRegions)
{
index = ~index - 1;
}
int index = _cacheEntries.BinarySearch(new CacheEntry(offset, 0, default));
if (index >= 0)
{
entry = _cacheEntries[index];
entryIndex = index;
return true;
if (index < 0)
{
index = ~index - 1;
}
if (index >= 0)
{
entry = _cacheEntries[index];
entryIndex = index;
return true;
}
}
}

View File

@@ -1,4 +1,6 @@
using ARMeilleure.Memory;
using Humanizer;
using Ryujinx.Common.Logging;
using Ryujinx.Memory;
using System;
using System.Collections.Generic;
@@ -15,9 +17,8 @@ namespace Ryujinx.Cpu.LightningJit.Cache
private static readonly int _pageMask = _pageSize - 1;
private const int CodeAlignment = 4; // Bytes.
private const int CacheSize = 2047 * 1024 * 1024;
private const int CacheSize = 128 * 1024 * 1024;
private static ReservedRegion _jitRegion;
private static JitCacheInvalidation _jitCacheInvalidator;
private static CacheMemoryAllocator _cacheAllocator;
@@ -26,6 +27,8 @@ namespace Ryujinx.Cpu.LightningJit.Cache
private static readonly Lock _lock = new();
private static bool _initialized;
private static readonly List<ReservedRegion> _jitRegions = new();
private static int _activeRegionIndex = 0;
[SupportedOSPlatform("windows")]
[LibraryImport("kernel32.dll", SetLastError = true)]
@@ -45,7 +48,9 @@ namespace Ryujinx.Cpu.LightningJit.Cache
return;
}
_jitRegion = new ReservedRegion(allocator, CacheSize);
var firstRegion = new ReservedRegion(allocator, CacheSize);
_jitRegions.Add(firstRegion);
_activeRegionIndex = 0;
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
{
@@ -65,8 +70,8 @@ namespace Ryujinx.Cpu.LightningJit.Cache
Debug.Assert(_initialized);
int funcOffset = Allocate(code.Length);
nint funcPtr = _jitRegion.Pointer + funcOffset;
ReservedRegion targetRegion = _jitRegions[_activeRegionIndex];
nint funcPtr = targetRegion.Pointer + funcOffset;
if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{
@@ -80,18 +85,11 @@ namespace Ryujinx.Cpu.LightningJit.Cache
}
else
{
ReprotectAsWritable(funcOffset, code.Length);
code.CopyTo(new Span<byte>((void*)funcPtr, code.Length));
ReprotectAsExecutable(funcOffset, code.Length);
ReprotectAsWritable(targetRegion, funcOffset, code.Length);
Marshal.Copy(code.ToArray(), 0, funcPtr, code.Length);
ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{
FlushInstructionCache(Process.GetCurrentProcess().Handle, funcPtr, (nuint)code.Length);
}
else
{
_jitCacheInvalidator?.Invalidate(funcPtr, (ulong)code.Length);
}
_jitCacheInvalidator?.Invalidate(funcPtr, (ulong)code.Length);
}
Add(funcOffset, code.Length);
@@ -106,50 +104,80 @@ namespace Ryujinx.Cpu.LightningJit.Cache
{
Debug.Assert(_initialized);
int funcOffset = (int)(pointer.ToInt64() - _jitRegion.Pointer.ToInt64());
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
foreach (var region in _jitRegions)
{
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
_cacheEntries.RemoveAt(entryIndex);
if (pointer.ToInt64() < region.Pointer.ToInt64() ||
pointer.ToInt64() >= (region.Pointer + CacheSize).ToInt64())
{
continue;
}
int funcOffset = (int)(pointer.ToInt64() - region.Pointer.ToInt64());
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
{
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
_cacheEntries.RemoveAt(entryIndex);
}
return;
}
}
}
private static void ReprotectAsWritable(int offset, int size)
private static void ReprotectAsWritable(ReservedRegion region, int offset, int size)
{
int endOffs = offset + size;
int regionStart = offset & ~_pageMask;
int regionEnd = (endOffs + _pageMask) & ~_pageMask;
_jitRegion.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
region.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
}
private static void ReprotectAsExecutable(int offset, int size)
private static void ReprotectAsExecutable(ReservedRegion region, int offset, int size)
{
int endOffs = offset + size;
int regionStart = offset & ~_pageMask;
int regionEnd = (endOffs + _pageMask) & ~_pageMask;
_jitRegion.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
region.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
}
private static int Allocate(int codeSize)
{
codeSize = AlignCodeSize(codeSize);
int allocOffset = _cacheAllocator.Allocate(codeSize);
if (allocOffset < 0)
for (int i = _activeRegionIndex; i < _jitRegions.Count; i++)
{
throw new OutOfMemoryException("JIT Cache exhausted.");
int allocOffset = _cacheAllocator.Allocate(codeSize);
if (allocOffset >= 0)
{
_jitRegions[i].ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
_activeRegionIndex = i;
return allocOffset;
}
}
_jitRegion.ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
int exhaustedRegion = _activeRegionIndex;
var newRegion = new ReservedRegion(_jitRegions[0].Allocator, CacheSize);
_jitRegions.Add(newRegion);
_activeRegionIndex = _jitRegions.Count - 1;
int newRegionNumber = _activeRegionIndex;
return allocOffset;
Logger.Warning?.Print(LogClass.Cpu, $"JIT Cache Region {exhaustedRegion} exhausted, creating new Cache Region {newRegionNumber} ({((newRegionNumber + 1) * CacheSize).Bytes()} Total Allocation).");
_cacheAllocator = new CacheMemoryAllocator(CacheSize);
int allocOffsetNew = _cacheAllocator.Allocate(codeSize);
if (allocOffsetNew < 0)
{
throw new OutOfMemoryException("Failed to allocate in new Cache Region!");
}
newRegion.ExpandIfNeeded((ulong)allocOffsetNew + (ulong)codeSize);
return allocOffsetNew;
}
private static int AlignCodeSize(int codeSize)

View File

@@ -150,6 +150,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
{ BsdSocketOption.SoLinger, SocketOptionName.Linger },
{ BsdSocketOption.SoOobInline, SocketOptionName.OutOfBandInline },
{ BsdSocketOption.SoReusePort, SocketOptionName.ReuseAddress },
{ BsdSocketOption.SoNoSigpipe, SocketOptionName.DontLinger },
{ BsdSocketOption.SoSndBuf, SocketOptionName.SendBuffer },
{ BsdSocketOption.SoRcvBuf, SocketOptionName.ReceiveBuffer },
{ BsdSocketOption.SoSndLoWat, SocketOptionName.SendLowWater },

View File

@@ -2522,106 +2522,6 @@
"zh_TW": "在 macOS 的應用程式資料夾中建立捷徑,啟動選取的應用程式"
}
},
{
"ID": "GameListContextMenuShowCompatEntry",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Show Compatibility Entry",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "GameListContextMenuShowCompatEntryToolTip",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Show the selected game in the Compatibility List you can normally access via the Help menu.",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "GameListContextMenuShowGameData",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Show Game Stats",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "GameListContextMenuShowGameDataToolTip",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Show the other various information about the currently selected game that is missing from the Grid view layout.",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "GameListContextMenuOpenModsDirectory",
"Translations": {

View File

@@ -32,9 +32,6 @@ namespace Ryujinx.Ava
public static MainWindow MainWindow => Current!
.ApplicationLifetime.Cast<IClassicDesktopStyleApplicationLifetime>()
.MainWindow.Cast<MainWindow>();
public static IClassicDesktopStyleApplicationLifetime AppLifetime => Current!
.ApplicationLifetime.Cast<IClassicDesktopStyleApplicationLifetime>();
public static bool IsClipboardAvailable(out IClipboard clipboard)
{

View File

@@ -19,18 +19,6 @@
Header="{ext:Locale GameListContextMenuCreateShortcut}"
Icon="{ext:Icon fa-solid fa-bookmark}"
ToolTip.Tip="{OnPlatform Default={ext:Locale GameListContextMenuCreateShortcutToolTip}, macOS={ext:Locale GameListContextMenuCreateShortcutToolTipMacOS}}" />
<MenuItem
IsVisible="{Binding HasCompatibilityEntry}"
Click="OpenApplicationCompatibility_Click"
Header="{ext:Locale GameListContextMenuShowCompatEntry}"
Icon="{ext:Icon mdi-gamepad}"
ToolTip.Tip="{ext:Locale GameListContextMenuShowCompatEntryToolTip}"/>
<MenuItem
IsVisible="{Binding IsGrid}"
Click="OpenApplicationData_Click"
Header="{ext:Locale GameListContextMenuShowGameData}"
Icon="{ext:Icon mdi-chart-line}"
ToolTip.Tip="{ext:Locale GameListContextMenuShowGameDataToolTip}"/>
<Separator />
<MenuItem
Click="OpenUserSaveDirectory_Click"
@@ -86,6 +74,7 @@
Header="{ext:Locale GameListContextMenuTrimXCI}"
IsEnabled="{Binding TrimXCIEnabled}"
ToolTip.Tip="{ext:Locale GameListContextMenuTrimXCIToolTip}" />
<Separator />
<MenuItem Header="{ext:Locale GameListContextMenuCacheManagement}" Icon="{ext:Icon mdi-cached}">
<MenuItem
Click="PurgePtcCache_Click"
@@ -123,7 +112,6 @@
Header="{ext:Locale GameListContextMenuExtractDataRomFS}"
ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataRomFSToolTip}" />
<MenuItem
IsVisible="{Binding HasDlc}"
Click="ExtractAocRomFs_Click"
Header="{ext:Locale GameListContextMenuExtractDataAocRomFS}"
ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataAocRomFSToolTip}" />

View File

@@ -12,7 +12,6 @@ using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Ava.Utilities;
using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Ava.Utilities.Compat;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Helper;
using Ryujinx.HLE.HOS;
@@ -334,7 +333,7 @@ namespace Ryujinx.Ava.UI.Controls
if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
return;
DownloadableContentModel selectedDlc = await DlcSelectView.Show(viewModel.SelectedApplication.Id, viewModel.ApplicationLibrary);
DownloadableContentModel selectedDlc = await DlcSelectView.Show(viewModel.SelectedApplication.IdBase, viewModel.ApplicationLibrary);
if (selectedDlc is not null)
{
@@ -386,18 +385,6 @@ namespace Ryujinx.Ava.UI.Controls
viewModel.SelectedApplication.Icon
);
}
public async void OpenApplicationCompatibility_Click(object sender, RoutedEventArgs args)
{
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
await CompatibilityList.Show(viewModel.SelectedApplication.IdString);
}
public async void OpenApplicationData_Click(object sender, RoutedEventArgs args)
{
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
await ApplicationDataView.Show(viewModel.SelectedApplication);
}
public async void RunApplication_Click(object sender, RoutedEventArgs args)
{
@@ -407,8 +394,12 @@ namespace Ryujinx.Ava.UI.Controls
public async void TrimXCI_Click(object sender, RoutedEventArgs args)
{
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
MainWindowViewModel viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
if (viewModel?.SelectedApplication != null)
{
await viewModel.TrimXCIFile(viewModel.SelectedApplication.Path);
}
}
}
}

View File

@@ -1,116 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:appLibrary="using:Ryujinx.Ava.Utilities.AppLibrary"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ryujinx.Ava.UI.Controls.ApplicationDataView"
x:DataType="appLibrary:ApplicationData">
<StackPanel Orientation="Vertical">
<Grid
RowDefinitions="Auto, Auto, Auto, Auto"
ColumnDefinitions="*">
<TextBlock Grid.Row="0"
HorizontalAlignment="Center"
Padding="0, -2, 0, 5"
Classes="h1"
Text="{Binding Name}" />
<TextBlock Grid.Row="1"
HorizontalAlignment="Center"
Text="{Binding Version}"
TextAlignment="Start"
TextWrapping="Wrap" />
<TextBlock Grid.Row="2"
HorizontalAlignment="Center"
Text="{Binding Developer}"
TextAlignment="Start"
TextWrapping="Wrap" />
<StackPanel Grid.ColumnSpan="2" Grid.Row="3"
HorizontalAlignment="Center"
Orientation="Horizontal" Margin="0 10, 0, 0"
Spacing="5">
<Button
Click="PlayabilityStatus_OnClick"
HorizontalContentAlignment="Left"
VerticalAlignment="Center"
IsVisible="{Binding HasPlayabilityInfo}"
Background="{DynamicResource AppListBackgroundColor}"
Padding="0">
<TextBlock
Margin="1.5"
Tag="{Binding IdString}"
Text="{Binding LocalizedStatus}"
Foreground="{Binding PlayabilityStatus, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
TextAlignment="Start"
TextWrapping="Wrap" />
<Button.Styles>
<Style Selector="Button">
<Setter Property="MinWidth"
Value="0" />
<!-- avoids very wide buttons from the overall project avalonia style -->
</Style>
</Button.Styles>
</Button>
<Button
Click="IdString_OnClick"
HorizontalContentAlignment="Left"
VerticalAlignment="Center"
Background="{DynamicResource AppListBackgroundColor}"
Padding="0">
<TextBlock
Margin="1.5"
HorizontalAlignment="Stretch"
Text="{Binding IdString}"
TextAlignment="Start"
TextWrapping="Wrap" />
</Button>
</StackPanel>
</Grid>
<Separator Margin="0, 10, 0, 10" />
<Grid ColumnDefinitions="Auto,*,Auto">
<StackPanel Grid.Column="0"
Margin="10,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Orientation="Vertical"
Spacing="5">
<TextBlock
HorizontalAlignment="Stretch"
Text="{Binding FileExtension}"
TextAlignment="Start"
TextWrapping="Wrap" />
<TextBlock
HorizontalAlignment="Stretch"
Text="{Binding Converter={helpers:MultiplayerInfoConverter}}"
TextAlignment="Start"
TextWrapping="Wrap" />
</StackPanel>
<StackPanel
Grid.Column="2"
Margin="0,0,10,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Orientation="Vertical"
Spacing="5">
<TextBlock
HorizontalAlignment="Stretch"
Text="{Binding LastPlayedString}"
TextAlignment="End"
TextWrapping="Wrap" />
<TextBlock
HorizontalAlignment="Stretch"
Text="{Binding TimePlayedString}"
IsVisible="{Binding HasPlayedPreviously}"
TextAlignment="End"
TextWrapping="Wrap" />
<TextBlock
HorizontalAlignment="Stretch"
Text="{Binding FileSizeString}"
TextAlignment="End"
TextWrapping="Wrap" />
</StackPanel>
</Grid>
</StackPanel>
</UserControl>

View File

@@ -1,84 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Styling;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Ava.Utilities.Compat;
using System.Linq;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.Controls
{
public partial class ApplicationDataView : UserControl
{
public static async Task Show(ApplicationData appData)
{
ContentDialog contentDialog = new()
{
PrimaryButtonText = string.Empty,
SecondaryButtonText = string.Empty,
CloseButtonText = LocaleManager.Instance[LocaleKeys.SettingsButtonClose],
Content = new ApplicationDataView { DataContext = appData }
};
Style closeButton = new(x => x.Name("CloseButton"));
closeButton.Setters.Add(new Setter(WidthProperty, 160d));
Style closeButtonParent = new(x => x.Name("CommandSpace"));
closeButtonParent.Setters.Add(new Setter(HorizontalAlignmentProperty,
Avalonia.Layout.HorizontalAlignment.Center));
contentDialog.Styles.Add(closeButton);
contentDialog.Styles.Add(closeButtonParent);
await ContentDialogHelper.ShowAsync(contentDialog);
}
public ApplicationDataView()
{
InitializeComponent();
}
private async void PlayabilityStatus_OnClick(object sender, RoutedEventArgs e)
{
if (sender is not Button { Content: TextBlock playabilityLabel })
return;
if (RyujinxApp.AppLifetime.Windows.TryGetFirst(x => x is ContentDialogOverlayWindow, out Window window))
window.Close(ContentDialogResult.None);
await CompatibilityList.Show((string)playabilityLabel.Tag);
}
private async void IdString_OnClick(object sender, RoutedEventArgs e)
{
if (DataContext is not MainWindowViewModel mwvm)
return;
if (sender is not Button { Content: TextBlock idText })
return;
if (!RyujinxApp.IsClipboardAvailable(out IClipboard clipboard))
return;
ApplicationData appData = mwvm.Applications.FirstOrDefault(it => it.IdString == idText.Text);
if (appData is null)
return;
await clipboard.SetTextAsync(appData.IdString);
NotificationHelper.ShowInformation(
"Copied Title ID",
$"{appData.Name} ({appData.IdString})");
}
}
}

View File

@@ -86,29 +86,13 @@
Text="{Binding Version}"
TextAlignment="Start"
TextWrapping="Wrap" />
<Button
Click="PlayabilityStatus_OnClick"
HorizontalContentAlignment="Left"
VerticalAlignment="Center"
<TextBlock
IsVisible="{Binding HasPlayabilityInfo}"
Background="{DynamicResource AppListBackgroundColor}"
Margin="-1, 0, 0, 0"
Padding="0" >
<TextBlock
Margin="1.5"
Tag="{Binding IdString}"
Text="{Binding LocalizedStatus}"
Foreground="{Binding PlayabilityStatus, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
TextAlignment="Start"
TextWrapping="Wrap" />
<Button.Styles>
<Style Selector="Button">
<Setter Property="MinWidth"
Value="0" />
<!-- avoids very wide buttons from the overall project avalonia style -->
</Style>
</Button.Styles>
</Button>
HorizontalAlignment="Stretch"
Text="{Binding LocalizedStatus}"
Foreground="{Binding PlayabilityStatus, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
TextAlignment="Start"
TextWrapping="Wrap" />
</StackPanel>
</Border>
<StackPanel

View File

@@ -5,9 +5,7 @@ using Avalonia.Interactivity;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Ava.Utilities.Compat;
using System;
using System.Globalization;
using System.Linq;
namespace Ryujinx.Ava.UI.Controls
@@ -30,17 +28,6 @@ namespace Ryujinx.Ava.UI.Controls
if (sender is ListBox { SelectedItem: ApplicationData selected })
RaiseEvent(new ApplicationOpenedEventArgs(selected, ApplicationOpenedEvent));
}
private async void PlayabilityStatus_OnClick(object sender, RoutedEventArgs e)
{
if (DataContext is not MainWindowViewModel mwvm)
return;
if (sender is not Button { Content: TextBlock playabilityLabel })
return;
await CompatibilityList.Show((string)playabilityLabel.Tag);
}
private async void IdString_OnClick(object sender, RoutedEventArgs e)
{

View File

@@ -14,7 +14,9 @@ namespace Ryujinx.Ava.UI.ViewModels
public DlcSelectViewModel(ulong titleId, ApplicationLibrary appLibrary)
{
_dlcs = appLibrary.FindDlcsFor(titleId)
_dlcs = appLibrary.DownloadableContents.Items
.Where(x => x.Dlc.TitleIdBase == titleId)
.Select(x => x.Dlc)
.OrderBy(it => it.IsBundled ? 0 : 1)
.ThenBy(it => it.TitleId)
.ToArray();

View File

@@ -69,7 +69,8 @@ namespace Ryujinx.Ava.UI.ViewModels
private void LoadDownloadableContents()
{
(DownloadableContentModel Dlc, bool IsEnabled)[] dlcs = _applicationLibrary.FindDlcConfigurationFor(_applicationData.Id);
IEnumerable<(DownloadableContentModel Dlc, bool IsEnabled)> dlcs = _applicationLibrary.DownloadableContents.Items
.Where(it => it.Dlc.TitleIdBase == _applicationData.IdBase);
bool hasBundledContent = false;
foreach ((DownloadableContentModel dlc, bool isEnabled) in dlcs)

View File

@@ -349,10 +349,6 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
public bool HasCompatibilityEntry => SelectedApplication.HasPlayabilityInfo;
public bool HasDlc => ApplicationLibrary.HasDlcs(SelectedApplication.Id);
public bool OpenUserSaveDirectoryEnabled => SelectedApplication.HasControlHolder && SelectedApplication.ControlHolder.Value.UserAccountSaveDataSize > 0;
public bool OpenDeviceSaveDirectoryEnabled => SelectedApplication.HasControlHolder && SelectedApplication.ControlHolder.Value.DeviceSaveDataSize > 0;

View File

@@ -41,7 +41,8 @@ namespace Ryujinx.Ava.UI.ViewModels
private void LoadUpdates()
{
(TitleUpdateModel TitleUpdate, bool IsSelected)[] updates = ApplicationLibrary.FindUpdateConfigurationFor(ApplicationData.Id);
IEnumerable<(TitleUpdateModel TitleUpdate, bool IsSelected)> updates = ApplicationLibrary.TitleUpdates.Items
.Where(it => it.TitleUpdate.TitleIdBase == ApplicationData.IdBase);
bool hasBundledContent = false;
SelectedUpdate = new TitleUpdateViewModelNoUpdate();

View File

@@ -50,7 +50,7 @@ namespace Ryujinx.Ava.UI.Views.Main
UninstallFileTypesMenuItem.Command = Commands.Create(UninstallFileTypes);
XciTrimmerMenuItem.Command = Commands.Create(XCITrimmerWindow.Show);
AboutWindowMenuItem.Command = Commands.Create(AboutWindow.Show);
CompatibilityListMenuItem.Command = Commands.Create(() => CompatibilityList.Show());
CompatibilityListMenuItem.Command = Commands.Create(CompatibilityList.Show);
UpdateMenuItem.Command = Commands.Create(async () =>
{

View File

@@ -128,50 +128,13 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
DynamicData.Kernel.Optional<ApplicationData> appData = Applications.Lookup(id);
if (appData.HasValue)
return appData.Value.Name;
if (!DownloadableContents.Keys.FindFirst(x => x.TitleId == id).TryGet(out DownloadableContentModel dlcData))
return id.ToString("X16");
string name = Path.GetFileNameWithoutExtension(dlcData.FileName)!;
int idx = name.IndexOf('[');
if (idx != -1)
name = name[..idx];
if (DownloadableContents.Keys.FindFirst(x => x.TitleId == id).TryGet(out DownloadableContentModel dlcData))
return Path.GetFileNameWithoutExtension(dlcData.FileName);
return name;
return id.ToString("X16");
}
public bool FindApplication(ulong id, out ApplicationData foundData)
{
DynamicData.Kernel.Optional<ApplicationData> appData = Applications.Lookup(id);
foundData = appData.HasValue ? appData.Value : null;
return appData.HasValue;
}
public bool FindUpdate(ulong id, out TitleUpdateModel foundData)
{
Gommon.Optional<TitleUpdateModel> appData =
TitleUpdates.Keys.FindFirst(x => x.TitleId == id);
foundData = appData.HasValue ? appData.Value : null;
return appData.HasValue;
}
public TitleUpdateModel[] FindUpdatesFor(ulong id)
=> TitleUpdates.Keys.Where(x => x.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
public (TitleUpdateModel TitleUpdate, bool IsSelected)[] FindUpdateConfigurationFor(ulong id)
=> TitleUpdates.Items.Where(x => x.TitleUpdate.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
public DownloadableContentModel[] FindDlcsFor(ulong id)
=> DownloadableContents.Keys.Where(x => x.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
public (DownloadableContentModel Dlc, bool IsEnabled)[] FindDlcConfigurationFor(ulong id)
=> DownloadableContents.Items.Where(x => x.Dlc.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
public bool HasDlcs(ulong id)
=> DownloadableContents.Keys.Any(x => x.TitleIdBase == (id & ~0x1FFFUL));
/// <exception cref="LibHac.Common.Keys.MissingKeyException">The configured key set is missing a key.</exception>
/// <exception cref="InvalidDataException">The NCA header could not be decrypted.</exception>
/// <exception cref="NotSupportedException">The NCA version is not supported.</exception>

View File

@@ -113,17 +113,20 @@ namespace Ryujinx.Ava.Utilities.Compat
.Select(FormatLabelName)
.JoinToString(", ");
public override string ToString() =>
new StringBuilder("CompatibilityEntry: {")
.Append($"{nameof(GameName)}=\"{GameName}\", ")
.Append($"{nameof(TitleId)}={TitleId}, ")
.Append($"{nameof(Labels)}={
Labels.FormatCollection(it => $"\"{it}\"", separator: ", ", prefix: "[", suffix: "]")
}, ")
.Append($"{nameof(Status)}=\"{Status}\", ")
.Append($"{nameof(LastUpdated)}=\"{LastUpdated}\"")
.Append('}')
.ToString();
public override string ToString()
{
StringBuilder sb = new("CompatibilityEntry: {");
sb.Append($"{nameof(GameName)}=\"{GameName}\", ");
sb.Append($"{nameof(TitleId)}={TitleId}, ");
sb.Append($"{nameof(Labels)}={
Labels.FormatCollection(it => $"\"{it}\"", separator: ", ", prefix: "[", suffix: "]")
}, ");
sb.Append($"{nameof(Status)}=\"{Status}\", ");
sb.Append($"{nameof(LastUpdated)}=\"{LastUpdated}\"");
sb.Append('}');
return sb.ToString();
}
public static string FormatLabelName(string labelName) => labelName.ToLower() switch
{

View File

@@ -34,7 +34,7 @@
Text="{ext:Locale CompatibilityListWarning}" />
</Grid>
<Grid Grid.Row="1" ColumnDefinitions="*,Auto,Auto">
<TextBox Name="SearchBox" Grid.Column="0" HorizontalAlignment="Stretch" Watermark="{ext:Locale CompatibilityListSearchBoxWatermark}" TextChanged="TextBox_OnTextChanged" />
<TextBox Grid.Column="0" HorizontalAlignment="Stretch" Watermark="{ext:Locale CompatibilityListSearchBoxWatermark}" TextChanged="TextBox_OnTextChanged" />
<CheckBox Grid.Column="1" Margin="7, 0, 0, 0" IsChecked="{Binding OnlyShowOwnedGames}" />
<TextBlock Grid.Column="2" Margin="-10, 0, 0, 0" Text="{ext:Locale CompatibilityListOnlyShowOwnedGames}" />
</Grid>

View File

@@ -9,7 +9,7 @@ namespace Ryujinx.Ava.Utilities.Compat
{
public partial class CompatibilityList : UserControl
{
public static async Task Show(string titleId = null)
public static async Task Show()
{
ContentDialog contentDialog = new()
{
@@ -18,10 +18,7 @@ namespace Ryujinx.Ava.Utilities.Compat
CloseButtonText = LocaleManager.Instance[LocaleKeys.SettingsButtonClose],
Content = new CompatibilityList
{
DataContext = new CompatibilityViewModel(RyujinxApp.MainWindow.ViewModel.ApplicationLibrary),
SearchBox = {
Text = titleId ?? ""
}
DataContext = new CompatibilityViewModel(RyujinxApp.MainWindow.ViewModel.ApplicationLibrary)
}
};