Compare commits

...

4 Commits

Author SHA1 Message Date
Evan Husted
1eb78872d8 misc: chore: annoyed I missed this, it's very obvious 2025-02-10 19:51:28 -06:00
Evan Husted
fe9fe2a10f UI: Added option to check for updates in the background 2025-02-10 19:28:46 -06:00
Evan Husted
6ab899f621 misc: chore: [ci skip] Use explicit types & target-typed new 2025-02-10 16:44:26 -06:00
FluffyOMC
faacec9801 JIT Cache Regions + HLE SoNoSigpipe BSD socket mapping (#615)
Instead of one big 2048MB JIT Cache that'd crash the emulator when maxed
out, we now have it where we add 256MB JIT Cache regions when needed,
helping reduce allocated memory where games don't use the JIT cache for
it, and helping bigger games that DO need JIT cache bigger than 2048MB!

![image](https://github.com/user-attachments/assets/ff17dc48-6028-4377-8c73-746ab21ab83b)
(SSBU goes past the 2048MB JIT Cache limit that would normally crash
Ryujinx ^)

Also I added a BSD socket that Baba is You's networking for downloading
custom levels uses.
2025-02-10 15:53:44 -06:00
18 changed files with 356 additions and 140 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 = 256 * 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);
ReservedRegion firstRegion = new(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 (ReservedRegion 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 (ReservedRegion _ 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

@@ -144,17 +144,15 @@ namespace ARMeilleure.Translation.PTC
public List<ulong> GetBlacklistedFunctions()
{
List<ulong> funcs = new List<ulong>();
List<ulong> funcs = [];
foreach (var profiledFunc in ProfiledFuncs)
foreach ((ulong ptr, FuncProfile funcProfile) in ProfiledFuncs)
{
if (profiledFunc.Value.Blacklist)
{
if (!funcs.Contains(profiledFunc.Key))
{
funcs.Add(profiledFunc.Key);
}
}
if (!funcProfile.Blacklist)
continue;
if (!funcs.Contains(ptr))
funcs.Add(ptr);
}
return funcs;

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 = 256 * 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);
ReservedRegion firstRegion = new(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 (ReservedRegion 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;
ReservedRegion newRegion = new(_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

@@ -12,7 +12,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache
{
private const int CodeAlignment = 4; // Bytes.
private const int SharedCacheSize = 2047 * 1024 * 1024;
private const int LocalCacheSize = 128 * 1024 * 1024;
private const int LocalCacheSize = 256 * 1024 * 1024;
// How many calls to the same function we allow until we pad the shared cache to force the function to become available there
// and allow the guest to take the fast path.

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

@@ -3350,26 +3350,101 @@
{
"ID": "SettingsTabGeneralCheckUpdatesOnLaunch",
"Translations": {
"ar_SA": "التحقق من وجود تحديثات عند التشغيل",
"de_DE": "Beim Start nach Updates suchen",
"el_GR": "Έλεγχος για Ενημερώσεις στην Εκκίνηση",
"en_US": "Check for Updates on Launch",
"es_ES": "Buscar actualizaciones al iniciar",
"fr_FR": "Vérifier les mises à jour au démarrage",
"he_IL": "בדוק אם קיימים עדכונים בהפעלה",
"it_IT": "Controlla aggiornamenti all'avvio",
"ja_JP": "起動時にアップデートを確認する",
"ko_KR": "시작 시, 업데이트 확인",
"no_NO": "Se etter oppdateringer ved oppstart",
"pl_PL": "Sprawdzaj aktualizacje przy uruchomieniu",
"pt_BR": "Verificar se há atualizações ao iniciar",
"ru_RU": "Проверять наличие обновлений при запуске",
"sv_SE": "Leta efter uppdatering vid uppstart",
"th_TH": "ตรวจหาการอัปเดตเมื่อเปิดโปรแกรม",
"tr_TR": "Her Açılışta Güncellemeleri Denetle",
"uk_UA": "Перевіряти наявність оновлень під час запуску",
"zh_CN": "启动时检查更新",
"zh_TW": "啟動時檢查更新"
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Check for Updates:",
"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": "SettingsTabGeneralCheckUpdatesOnLaunchOff",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Off",
"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": "SettingsTabGeneralCheckUpdatesOnLaunchPromptAtStartup",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Prompt",
"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": "SettingsTabGeneralCheckUpdatesOnLaunchBackground",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Background",
"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": ""
}
},
{

View File

@@ -13,6 +13,7 @@ using Ryujinx.Ava.UI.Models.Input;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Ava.Utilities.Configuration;
using Ryujinx.Ava.Utilities.Configuration.System;
using Ryujinx.Ava.Utilities.Configuration.UI;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Multiplayer;
using Ryujinx.Common.GraphicsDriver;
@@ -121,6 +122,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool RememberWindowState { get; set; }
public bool ShowTitleBar { get; set; }
public int HideCursor { get; set; }
public int UpdateCheckerType { get; set; }
public bool EnableDockedMode { get; set; }
public bool EnableKeyboard { get; set; }
public bool EnableMouse { get; set; }
@@ -476,6 +478,7 @@ namespace Ryujinx.Ava.UI.ViewModels
RememberWindowState = config.RememberWindowState;
ShowTitleBar = config.ShowTitleBar;
HideCursor = (int)config.HideCursor.Value;
UpdateCheckerType = (int)config.UpdateCheckerType.Value;
GameDirectories.Clear();
GameDirectories.AddRange(config.UI.GameDirs.Value);
@@ -582,6 +585,7 @@ namespace Ryujinx.Ava.UI.ViewModels
config.RememberWindowState.Value = RememberWindowState;
config.ShowTitleBar.Value = ShowTitleBar;
config.HideCursor.Value = (HideCursorMode)HideCursor;
config.UpdateCheckerType.Value = (UpdaterType)UpdateCheckerType;
if (GameDirectoryChanged)
{

View File

@@ -6,6 +6,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
xmlns:helper="clr-namespace:Ryujinx.Common.Helper;assembly=Ryujinx.Common"
mc:Ignorable="d"
x:DataType="viewModels:SettingsViewModel">
<Design.DataContext>
@@ -30,18 +31,33 @@
ToolTip.Tip="{ext:Locale ToggleDiscordTooltip}"
Text="{ext:Locale SettingsTabGeneralEnableDiscordRichPresence}" />
</CheckBox>
<CheckBox IsChecked="{Binding CheckUpdatesOnStart}">
<TextBlock Text="{ext:Locale SettingsTabGeneralCheckUpdatesOnLaunch}" />
</CheckBox>
<CheckBox IsChecked="{Binding ShowConfirmExit}">
<TextBlock Text="{ext:Locale SettingsTabGeneralShowConfirmExitDialog}" />
</CheckBox>
<CheckBox IsChecked="{Binding RememberWindowState}">
<TextBlock Text="{ext:Locale SettingsTabGeneralRememberWindowState}" />
</CheckBox>
<CheckBox IsChecked="{Binding ShowTitleBar}" Name="ShowTitleBarBox">
<CheckBox IsChecked="{Binding ShowTitleBar}" IsVisible="{x:Static helper:RunningPlatform.IsWindows}">
<TextBlock Text="{ext:Locale SettingsTabGeneralShowTitleBar}" />
</CheckBox>
<StackPanel Margin="0, 15, 0, 0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
Text="{ext:Locale SettingsTabGeneralCheckUpdatesOnLaunch}"
Width="150" />
<ComboBox SelectedIndex="{Binding UpdateCheckerType}"
HorizontalContentAlignment="Left"
MinWidth="100">
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabGeneralCheckUpdatesOnLaunchOff}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabGeneralCheckUpdatesOnLaunchPromptAtStartup}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabGeneralCheckUpdatesOnLaunchBackground}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
<StackPanel Margin="0, 15, 0, 0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
Text="{ext:Locale SettingsTabGeneralHideCursor}"

View File

@@ -21,7 +21,6 @@ namespace Ryujinx.Ava.UI.Views.Settings
public SettingsUiView()
{
InitializeComponent();
ShowTitleBarBox.IsVisible = OperatingSystem.IsWindows();
AddGameDirButton.Command =
Commands.Create(() => AddDirButton(GameDirPathBox, ViewModel.GameDirectories, true));
AddAutoloadDirButton.Command =

View File

@@ -19,6 +19,7 @@ using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.Utilities;
using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Ava.Utilities.Configuration;
using Ryujinx.Ava.Utilities.Configuration.UI;
using Ryujinx.Common;
using Ryujinx.Common.Helper;
using Ryujinx.Common.Logging;
@@ -400,10 +401,29 @@ namespace Ryujinx.Ava.UI.Windows
await Dispatcher.UIThread.InvokeAsync(async () => await UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys));
}
if (ConfigurationState.Instance.CheckUpdatesOnStart && !CommandLineState.HideAvailableUpdates && Updater.CanUpdate())
if (!Updater.CanUpdate() || CommandLineState.HideAvailableUpdates)
return;
switch (ConfigurationState.Instance.UpdateCheckerType.Value)
{
await Updater.BeginUpdateAsync()
.Catch(task => Logger.Error?.Print(LogClass.Application, $"Updater Error: {task.Exception}"));
case UpdaterType.PromptAtStartup:
await Updater.BeginUpdateAsync()
.Catch(task => Logger.Error?.Print(LogClass.Application, $"Updater Error: {task.Exception}"));
break;
case UpdaterType.CheckInBackground:
if ((await Updater.CheckVersionAsync()).TryGet(out (Version Current, Version Incoming) versions))
{
string newVersionString = ReleaseInformation.IsCanaryBuild
? $"Canary {versions.Current} -> Canary {versions.Incoming}"
: $"{versions.Current} -> {versions.Incoming}";
if (versions.Current < versions.Incoming)
NotificationHelper.ShowInformation(
title: "Update Available",
text: newVersionString,
onClick: () => _ = Updater.BeginUpdateAsync());
}
break;
}
}

View File

@@ -43,7 +43,18 @@ namespace Ryujinx.Ava
private const int ConnectionCount = 4;
private static string _buildVer;
private static string _platformExt;
private static readonly string _platformExt =
RunningPlatform.IsMacOS
? "macos_universal.app.tar.gz"
: RunningPlatform.IsWindows
? "win_x64.zip"
: RunningPlatform.IsX64Linux
? "linux_x64.tar.gz"
: RunningPlatform.IsArmLinux
? "linux_arm64.tar.gz"
: throw new PlatformNotSupportedException();
private static string _buildUrl;
private static long _buildSize;
private static bool _updateSuccessful;
@@ -51,30 +62,8 @@ namespace Ryujinx.Ava
private static readonly string[] _windowsDependencyDirs = [];
public static async Task BeginUpdateAsync(bool showVersionUpToDate = false)
public static async Task<Optional<(Version Current, Version Incoming)>> CheckVersionAsync(bool showVersionUpToDate = false)
{
if (_running)
{
return;
}
_running = true;
// Detect current platform
if (OperatingSystem.IsMacOS())
{
_platformExt = "macos_universal.app.tar.gz";
}
else if (OperatingSystem.IsWindows())
{
_platformExt = "win_x64.zip";
}
else if (OperatingSystem.IsLinux())
{
string arch = RuntimeInformation.OSArchitecture == Architecture.Arm64 ? "arm64" : "x64";
_platformExt = $"linux_{arch}.tar.gz";
}
if (!Version.TryParse(Program.Version, out Version currentVersion))
{
Logger.Error?.Print(LogClass.Application, $"Failed to convert the current {RyujinxApp.FullAppName} version!");
@@ -85,7 +74,7 @@ namespace Ryujinx.Ava
_running = false;
return;
return default;
}
Logger.Info?.Print(LogClass.Application, "Checking for updates.");
@@ -123,7 +112,7 @@ namespace Ryujinx.Ava
_running = false;
return;
return default;
}
break;
@@ -149,7 +138,7 @@ namespace Ryujinx.Ava
_running = false;
return;
return default;
}
}
catch (Exception exception)
@@ -161,7 +150,7 @@ namespace Ryujinx.Ava
_running = false;
return;
return default;
}
if (!Version.TryParse(_buildVer, out Version newVersion))
@@ -174,9 +163,23 @@ namespace Ryujinx.Ava
_running = false;
return (currentVersion, null);
}
return (currentVersion, newVersion);
}
public static async Task BeginUpdateAsync(bool showVersionUpToDate = false)
{
if (_running)
{
return;
}
_running = true;
(Version currentVersion, Version newVersion) = (await CheckVersionAsync(showVersionUpToDate)).OrDefault();
if (newVersion <= currentVersion)
{
if (showVersionUpToDate)

View File

@@ -15,7 +15,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// <summary>
/// The current version of the file format
/// </summary>
public const int CurrentVersion = 64;
public const int CurrentVersion = 65;
/// <summary>
/// Version of the configuration file format
@@ -163,9 +163,14 @@ namespace Ryujinx.Ava.Utilities.Configuration
public bool EnableDiscordIntegration { get; set; }
/// <summary>
/// Checks for updates when Ryujinx starts when enabled
/// DEPRECATED: Checks for updates when Ryujinx starts when enabled
/// </summary>
public bool CheckUpdatesOnStart { get; set; }
/// <summary>
/// Checks for updates when Ryujinx starts when enabled, either prompting when an update is found or just showing a notification.
/// </summary>
public UpdaterType UpdateCheckerType { get; set; }
/// <summary>
/// Show "Confirm Exit" Dialog

View File

@@ -45,6 +45,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
EnableDiscordIntegration.Value = cff.EnableDiscordIntegration;
CheckUpdatesOnStart.Value = cff.CheckUpdatesOnStart;
UpdateCheckerType.Value = cff.UpdateCheckerType;
ShowConfirmExit.Value = cff.ShowConfirmExit;
RememberWindowState.Value = cff.RememberWindowState;
ShowTitleBar.Value = cff.ShowTitleBar;
@@ -431,7 +432,8 @@ namespace Ryujinx.Ava.Utilities.Configuration
}),
(62, static cff => cff.RainbowSpeed = 1f),
(63, static cff => cff.MatchSystemTime = false),
(64, static cff => cff.LoggingEnableAvalonia = false)
(64, static cff => cff.LoggingEnableAvalonia = false),
(65, static cff => cff.UpdateCheckerType = cff.CheckUpdatesOnStart ? UpdaterType.PromptAtStartup : UpdaterType.Off)
);
}
}

View File

@@ -1,6 +1,7 @@
using ARMeilleure;
using Gommon;
using Ryujinx.Ava.Utilities.Configuration.System;
using Ryujinx.Ava.Utilities.Configuration.UI;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid;
@@ -767,6 +768,11 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// Checks for updates when Ryujinx starts when enabled
/// </summary>
public ReactiveObject<bool> CheckUpdatesOnStart { get; private set; }
/// <summary>
/// Checks for updates when Ryujinx starts when enabled, either prompting when an update is found or just showing a notification.
/// </summary>
public ReactiveObject<UpdaterType> UpdateCheckerType { get; private set; }
/// <summary>
/// Show "Confirm Exit" Dialog
@@ -804,6 +810,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
Hacks = new HacksSection();
EnableDiscordIntegration = new ReactiveObject<bool>();
CheckUpdatesOnStart = new ReactiveObject<bool>();
UpdateCheckerType = new ReactiveObject<UpdaterType>();
ShowConfirmExit = new ReactiveObject<bool>();
RememberWindowState = new ReactiveObject<bool>();
ShowTitleBar = new ReactiveObject<bool>();

View File

@@ -56,6 +56,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
DockedMode = System.EnableDockedMode,
EnableDiscordIntegration = EnableDiscordIntegration,
CheckUpdatesOnStart = CheckUpdatesOnStart,
UpdateCheckerType = UpdateCheckerType,
ShowConfirmExit = ShowConfirmExit,
RememberWindowState = RememberWindowState,
ShowTitleBar = ShowTitleBar,
@@ -175,7 +176,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
System.SystemTimeOffset.Value = 0;
System.EnableDockedMode.Value = true;
EnableDiscordIntegration.Value = true;
CheckUpdatesOnStart.Value = true;
UpdateCheckerType.Value = UpdaterType.PromptAtStartup;
ShowConfirmExit.Value = true;
RememberWindowState.Value = true;
ShowTitleBar.Value = !OperatingSystem.IsWindows();

View File

@@ -0,0 +1,13 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Utilities.Configuration.UI
{
[JsonConverter(typeof(TypedStringEnumConverter<UpdaterType>))]
public enum UpdaterType
{
Off,
PromptAtStartup,
CheckInBackground
}
}

View File

@@ -152,7 +152,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result)
{
List<MessagePackObject> packedObjects = [];
foreach (var reportKey in ReportKeys)
foreach (string reportKey in ReportKeys)
{
if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
{
@@ -176,7 +176,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result)
{
Dictionary<string, MessagePackObject> packedObjects = [];
foreach (var reportKey in ReportKeys)
foreach (string reportKey in ReportKeys)
{
if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
continue;