Compare commits

...

3 Commits

Author SHA1 Message Date
TSRBerry
cc1a933a2f ModLoader: Fix case sensitivy issues (#4720)
* Fix case sensitivity for mod subdirectories

* Small refactoring of ModLoader
2023-05-03 02:07:16 +02:00
TSRBerry
dd574146fb Add hide-cursor command line argument & always hide cursor option (#4613)
* Add hide-cursor command line argument

* gtk: Adjust SettingsWindow for hide cursor options

* ava: Adjust SettingsWindow for hide cursor options

* ava: Add override check for HideCursor arg

* Remove copy&paste sins

* ava: Leave a little more room between the options

* gtk: Fix hide cursor issues

* ava: Only hide cursor if it's within the embedded window
2023-05-02 03:29:47 +02:00
riperiperi
2c94ac455e GPU: Keep rendered textures without any pool references alive (#4662)
* GPU: Keep sampled textures without any pool references alive

Occasionally games are very wasteful and clear/write to a texture without ever sampling it. As rendered textures in NVN games seem to all have overlapping memory ranges, the texture will eventually get overwritten.

Normally, this would trigger a removal from the auto delete cache, but a pool entry would keep the texture alive. However, with these textures that are never used, they will get deleted immediately and recreated on the next frame.

This change makes it so the ShortTextureCache can keep textures that have naver had a pool reference alive for a few frames, so they're not constantly being created and deleted.

This improves performance in Zelda BOTW a little.

* Cleanup
2023-05-01 16:27:51 -03:00
30 changed files with 499 additions and 251 deletions

View File

@@ -157,7 +157,7 @@ namespace Ryujinx.Ava
_isFirmwareTitle = true; _isFirmwareTitle = true;
} }
ConfigurationState.Instance.HideCursorOnIdle.Event += HideCursorState_Changed; ConfigurationState.Instance.HideCursor.Event += HideCursorState_Changed;
_topLevel.PointerMoved += TopLevel_PointerMoved; _topLevel.PointerMoved += TopLevel_PointerMoved;
@@ -468,9 +468,9 @@ namespace Ryujinx.Ava
(_rendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.MakeCurrent(null); (_rendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.MakeCurrent(null);
} }
private void HideCursorState_Changed(object sender, ReactiveEventArgs<bool> state) private void HideCursorState_Changed(object sender, ReactiveEventArgs<HideCursorMode> state)
{ {
if (state.NewValue) if (state.NewValue == HideCursorMode.OnIdle)
{ {
_lastCursorMoveTime = Stopwatch.GetTimestamp(); _lastCursorMoveTime = Stopwatch.GetTimestamp();
} }
@@ -964,22 +964,21 @@ namespace Ryujinx.Ava
NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat()); NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
if (_viewModel.IsActive) if (_viewModel.IsActive)
{
if (ConfigurationState.Instance.Hid.EnableMouse)
{ {
if (_isCursorInRenderer) if (_isCursorInRenderer)
{
if (ConfigurationState.Instance.Hid.EnableMouse)
{ {
HideCursor(); HideCursor();
} }
else else
{ {
switch (ConfigurationState.Instance.HideCursor.Value)
{
case HideCursorMode.Never:
ShowCursor(); ShowCursor();
} break;
} case HideCursorMode.OnIdle:
else
{
if (ConfigurationState.Instance.HideCursorOnIdle)
{
if (Stopwatch.GetTimestamp() - _lastCursorMoveTime >= CursorHideIdleTime * Stopwatch.Frequency) if (Stopwatch.GetTimestamp() - _lastCursorMoveTime >= CursorHideIdleTime * Stopwatch.Frequency)
{ {
HideCursor(); HideCursor();
@@ -988,8 +987,17 @@ namespace Ryujinx.Ava
{ {
ShowCursor(); ShowCursor();
} }
break;
case HideCursorMode.Always:
HideCursor();
break;
} }
} }
}
else
{
ShowCursor();
}
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
{ {

View File

@@ -80,7 +80,10 @@
"SettingsTabGeneralEnableDiscordRichPresence": "Enable Discord Rich Presence", "SettingsTabGeneralEnableDiscordRichPresence": "Enable Discord Rich Presence",
"SettingsTabGeneralCheckUpdatesOnLaunch": "Check for Updates on Launch", "SettingsTabGeneralCheckUpdatesOnLaunch": "Check for Updates on Launch",
"SettingsTabGeneralShowConfirmExitDialog": "Show \"Confirm Exit\" Dialog", "SettingsTabGeneralShowConfirmExitDialog": "Show \"Confirm Exit\" Dialog",
"SettingsTabGeneralHideCursorOnIdle": "Hide Cursor on Idle", "SettingsTabGeneralHideCursor": "Hide Cursor:",
"SettingsTabGeneralHideCursorNever": "Never",
"SettingsTabGeneralHideCursorOnIdle": "On Idle",
"SettingsTabGeneralHideCursorAlways": "Always",
"SettingsTabGeneralGameDirectories": "Game Directories", "SettingsTabGeneralGameDirectories": "Game Directories",
"SettingsTabGeneralAdd": "Add", "SettingsTabGeneralAdd": "Add",
"SettingsTabGeneralRemove": "Remove", "SettingsTabGeneralRemove": "Remove",

View File

@@ -183,6 +183,18 @@ namespace Ryujinx.Ava
{ {
ConfigurationState.Instance.System.EnableDockedMode.Value = CommandLineState.OverrideDockedMode.Value; ConfigurationState.Instance.System.EnableDockedMode.Value = CommandLineState.OverrideDockedMode.Value;
} }
// Check if HideCursor was overridden.
if (CommandLineState.OverrideHideCursor is not null)
{
ConfigurationState.Instance.HideCursor.Value = CommandLineState.OverrideHideCursor!.ToLower() switch
{
"never" => HideCursorMode.Never,
"onidle" => HideCursorMode.OnIdle,
"always" => HideCursorMode.Always,
_ => ConfigurationState.Instance.HideCursor.Value
};
}
} }
private static void PrintSystemInfo() private static void PrintSystemInfo()

View File

@@ -1576,8 +1576,8 @@ namespace Ryujinx.Ava.UI.ViewModels
{ {
if (SelectedApplication != null) if (SelectedApplication != null)
{ {
string modsBasePath = VirtualFileSystem.ModLoader.GetModsBasePath(); string modsBasePath = ModLoader.GetModsBasePath();
string titleModsPath = VirtualFileSystem.ModLoader.GetTitleDir(modsBasePath, SelectedApplication.TitleId); string titleModsPath = ModLoader.GetTitleDir(modsBasePath, SelectedApplication.TitleId);
OpenHelper.OpenFolder(titleModsPath); OpenHelper.OpenFolder(titleModsPath);
} }
@@ -1587,8 +1587,8 @@ namespace Ryujinx.Ava.UI.ViewModels
{ {
if (SelectedApplication != null) if (SelectedApplication != null)
{ {
string sdModsBasePath = VirtualFileSystem.ModLoader.GetSdModsBasePath(); string sdModsBasePath = ModLoader.GetSdModsBasePath();
string titleModsPath = VirtualFileSystem.ModLoader.GetTitleDir(sdModsBasePath, SelectedApplication.TitleId); string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, SelectedApplication.TitleId);
OpenHelper.OpenFolder(titleModsPath); OpenHelper.OpenFolder(titleModsPath);
} }

View File

@@ -132,7 +132,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool EnableDiscordIntegration { get; set; } public bool EnableDiscordIntegration { get; set; }
public bool CheckUpdatesOnStart { get; set; } public bool CheckUpdatesOnStart { get; set; }
public bool ShowConfirmExit { get; set; } public bool ShowConfirmExit { get; set; }
public bool HideCursorOnIdle { get; set; } public int HideCursor { get; set; }
public bool EnableDockedMode { get; set; } public bool EnableDockedMode { get; set; }
public bool EnableKeyboard { get; set; } public bool EnableKeyboard { get; set; }
public bool EnableMouse { get; set; } public bool EnableMouse { get; set; }
@@ -375,7 +375,7 @@ namespace Ryujinx.Ava.UI.ViewModels
EnableDiscordIntegration = config.EnableDiscordIntegration; EnableDiscordIntegration = config.EnableDiscordIntegration;
CheckUpdatesOnStart = config.CheckUpdatesOnStart; CheckUpdatesOnStart = config.CheckUpdatesOnStart;
ShowConfirmExit = config.ShowConfirmExit; ShowConfirmExit = config.ShowConfirmExit;
HideCursorOnIdle = config.HideCursorOnIdle; HideCursor = (int)config.HideCursor.Value;
GameDirectories.Clear(); GameDirectories.Clear();
GameDirectories.AddRange(config.Ui.GameDirs.Value); GameDirectories.AddRange(config.Ui.GameDirs.Value);
@@ -458,7 +458,7 @@ namespace Ryujinx.Ava.UI.ViewModels
config.EnableDiscordIntegration.Value = EnableDiscordIntegration; config.EnableDiscordIntegration.Value = EnableDiscordIntegration;
config.CheckUpdatesOnStart.Value = CheckUpdatesOnStart; config.CheckUpdatesOnStart.Value = CheckUpdatesOnStart;
config.ShowConfirmExit.Value = ShowConfirmExit; config.ShowConfirmExit.Value = ShowConfirmExit;
config.HideCursorOnIdle.Value = HideCursorOnIdle; config.HideCursor.Value = (HideCursorMode)HideCursor;
if (_directoryChanged) if (_directoryChanged)
{ {

View File

@@ -37,9 +37,24 @@
<CheckBox IsChecked="{Binding ShowConfirmExit}"> <CheckBox IsChecked="{Binding ShowConfirmExit}">
<TextBlock Text="{locale:Locale SettingsTabGeneralShowConfirmExitDialog}" /> <TextBlock Text="{locale:Locale SettingsTabGeneralShowConfirmExitDialog}" />
</CheckBox> </CheckBox>
<CheckBox IsChecked="{Binding HideCursorOnIdle}"> <StackPanel Margin="0, 15, 0, 10" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
Text="{locale:Locale SettingsTabGeneralHideCursor}"
Width="150" />
<ComboBox SelectedIndex="{Binding HideCursor}"
HorizontalContentAlignment="Left"
MinWidth="100">
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGeneralHideCursorNever}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGeneralHideCursorOnIdle}" /> <TextBlock Text="{locale:Locale SettingsTabGeneralHideCursorOnIdle}" />
</CheckBox> </ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGeneralHideCursorAlways}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
</StackPanel> </StackPanel>
<Separator Height="1" /> <Separator Height="1" />
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGeneralGameDirectories}" /> <TextBlock Classes="h1" Text="{locale:Locale SettingsTabGeneralGameDirectories}" />

View File

@@ -35,8 +35,8 @@ namespace Ryujinx.Ava.UI.Windows
InitializeComponent(); InitializeComponent();
string modsBasePath = virtualFileSystem.ModLoader.GetModsBasePath(); string modsBasePath = ModLoader.GetModsBasePath();
string titleModsPath = virtualFileSystem.ModLoader.GetTitleDir(modsBasePath, titleId); string titleModsPath = ModLoader.GetTitleDir(modsBasePath, titleId);
ulong titleIdValue = ulong.Parse(titleId, System.Globalization.NumberStyles.HexNumber); ulong titleIdValue = ulong.Parse(titleId, System.Globalization.NumberStyles.HexNumber);
_enabledCheatsPath = Path.Combine(titleModsPath, "cheats", "enabled.txt"); _enabledCheatsPath = Path.Combine(titleModsPath, "cheats", "enabled.txt");

View File

@@ -0,0 +1,9 @@
namespace Ryujinx.Common.Configuration
{
public enum HideCursorMode
{
Never,
OnIdle,
Always
}
}

View File

@@ -1,5 +1,4 @@
using System.Collections; using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Image namespace Ryujinx.Graphics.Gpu.Image
@@ -9,6 +8,7 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary> /// </summary>
class ShortTextureCacheEntry class ShortTextureCacheEntry
{ {
public bool IsAutoDelete;
public readonly TextureDescriptor Descriptor; public readonly TextureDescriptor Descriptor;
public readonly int InvalidatedSequence; public readonly int InvalidatedSequence;
public readonly Texture Texture; public readonly Texture Texture;
@@ -24,6 +24,17 @@ namespace Ryujinx.Graphics.Gpu.Image
InvalidatedSequence = texture.InvalidatedSequence; InvalidatedSequence = texture.InvalidatedSequence;
Texture = texture; Texture = texture;
} }
/// <summary>
/// Create a new entry on the short duration texture cache from the auto delete cache.
/// </summary>
/// <param name="texture">The texture</param>
public ShortTextureCacheEntry(Texture texture)
{
IsAutoDelete = true;
InvalidatedSequence = texture.InvalidatedSequence;
Texture = texture;
}
} }
/// <summary> /// <summary>
@@ -199,7 +210,11 @@ namespace Ryujinx.Graphics.Gpu.Image
{ {
texture.DecrementReferenceCount(); texture.DecrementReferenceCount();
if (!texture.ShortCacheEntry.IsAutoDelete)
{
_shortCacheLookup.Remove(texture.ShortCacheEntry.Descriptor); _shortCacheLookup.Remove(texture.ShortCacheEntry.Descriptor);
}
texture.ShortCacheEntry = null; texture.ShortCacheEntry = null;
} }
} }
@@ -222,6 +237,25 @@ namespace Ryujinx.Graphics.Gpu.Image
texture.IncrementReferenceCount(); texture.IncrementReferenceCount();
} }
/// <summary>
/// Adds a texture to the short duration cache without a descriptor. This typically keeps it alive for two ticks.
/// On expiry, it will be removed from the AutoDeleteCache.
/// </summary>
/// <param name="texture">Texture to add to the short cache</param>
public void AddShortCache(Texture texture)
{
if (texture.ShortCacheEntry != null)
{
var entry = new ShortTextureCacheEntry(texture);
_shortCacheBuilder.Add(entry);
texture.ShortCacheEntry = entry;
texture.IncrementReferenceCount();
}
}
/// <summary> /// <summary>
/// Delete textures from the short duration cache. /// Delete textures from the short duration cache.
/// Moves the builder set to be deleted on next process. /// Moves the builder set to be deleted on next process.
@@ -234,7 +268,15 @@ namespace Ryujinx.Graphics.Gpu.Image
{ {
entry.Texture.DecrementReferenceCount(); entry.Texture.DecrementReferenceCount();
if (entry.IsAutoDelete)
{
Remove(entry.Texture, false);
}
else
{
_shortCacheLookup.Remove(entry.Descriptor); _shortCacheLookup.Remove(entry.Descriptor);
}
entry.Texture.ShortCacheEntry = null; entry.Texture.ShortCacheEntry = null;
} }

View File

@@ -144,6 +144,11 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary> /// </summary>
public ShortTextureCacheEntry ShortCacheEntry { get; set; } public ShortTextureCacheEntry ShortCacheEntry { get; set; }
/// <summary>
/// Whether this texture has ever been referenced by a pool.
/// </summary>
public bool HadPoolOwner { get; private set; }
/// Physical memory ranges where the texture data is located. /// Physical memory ranges where the texture data is located.
/// </summary> /// </summary>
public MultiRange Range { get; private set; } public MultiRange Range { get; private set; }
@@ -1506,10 +1511,13 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <param name="gpuVa">GPU VA of the pool reference</param> /// <param name="gpuVa">GPU VA of the pool reference</param>
public void IncrementReferenceCount(TexturePool pool, int id, ulong gpuVa) public void IncrementReferenceCount(TexturePool pool, int id, ulong gpuVa)
{ {
HadPoolOwner = true;
lock (_poolOwners) lock (_poolOwners)
{ {
_poolOwners.Add(new TexturePoolOwner { Pool = pool, ID = id, GpuAddress = gpuVa }); _poolOwners.Add(new TexturePoolOwner { Pool = pool, ID = id, GpuAddress = gpuVa });
} }
_referenceCount++; _referenceCount++;
if (ShortCacheEntry != null) if (ShortCacheEntry != null)
@@ -1594,7 +1602,7 @@ namespace Ryujinx.Graphics.Gpu.Image
_poolOwners.Clear(); _poolOwners.Clear();
} }
if (ShortCacheEntry != null && _context.IsGpuThread()) if (ShortCacheEntry != null && !ShortCacheEntry.IsAutoDelete && _context.IsGpuThread())
{ {
// If this is called from another thread (unmapped), the short cache will // If this is called from another thread (unmapped), the short cache will
// have to remove this texture on a future tick. // have to remove this texture on a future tick.

View File

@@ -847,9 +847,19 @@ namespace Ryujinx.Graphics.Gpu.Image
setData |= modified || flush; setData |= modified || flush;
if (overlapInCache) if (overlapInCache)
{
if (flush || overlap.HadPoolOwner || overlap.IsView)
{ {
_cache.Remove(overlap, flush); _cache.Remove(overlap, flush);
} }
else
{
// This texture has only ever been referenced in the AutoDeleteCache.
// Keep this texture alive with the short duration cache, as it may be used often but not sampled.
_cache.AddShortCache(overlap);
}
}
removeOverlap = modified; removeOverlap = modified;
} }
@@ -1198,6 +1208,16 @@ namespace Ryujinx.Graphics.Gpu.Image
_cache.AddShortCache(texture, ref descriptor); _cache.AddShortCache(texture, ref descriptor);
} }
/// <summary>
/// Adds a texture to the short duration cache without a descriptor. This typically keeps it alive for two ticks.
/// On expiry, it will be removed from the AutoDeleteCache.
/// </summary>
/// <param name="texture">Texture to add to the short cache</param>
public void AddShortCache(Texture texture)
{
_cache.AddShortCache(texture);
}
/// <summary> /// <summary>
/// Removes a texture from the short duration cache. /// Removes a texture from the short duration cache.
/// </summary> /// </summary>

View File

@@ -89,7 +89,7 @@ namespace Ryujinx.HLE.HOS
} }
// Title independent mods // Title independent mods
public class PatchCache private class PatchCache
{ {
public List<Mod<DirectoryInfo>> NsoPatches { get; } public List<Mod<DirectoryInfo>> NsoPatches { get; }
public List<Mod<DirectoryInfo>> NroPatches { get; } public List<Mod<DirectoryInfo>> NroPatches { get; }
@@ -107,14 +107,14 @@ namespace Ryujinx.HLE.HOS
} }
} }
public Dictionary<ulong, ModCache> AppMods; // key is TitleId private readonly Dictionary<ulong, ModCache> _appMods; // key is TitleId
public PatchCache Patches; private PatchCache _patches;
private static readonly EnumerationOptions _dirEnumOptions; private static readonly EnumerationOptions DirEnumOptions;
static ModLoader() static ModLoader()
{ {
_dirEnumOptions = new EnumerationOptions DirEnumOptions = new EnumerationOptions
{ {
MatchCasing = MatchCasing.CaseInsensitive, MatchCasing = MatchCasing.CaseInsensitive,
MatchType = MatchType.Simple, MatchType = MatchType.Simple,
@@ -125,37 +125,73 @@ namespace Ryujinx.HLE.HOS
public ModLoader() public ModLoader()
{ {
AppMods = new Dictionary<ulong, ModCache>(); _appMods = new Dictionary<ulong, ModCache>();
Patches = new PatchCache(); _patches = new PatchCache();
} }
public void Clear() private void Clear()
{ {
AppMods.Clear(); _appMods.Clear();
Patches = new PatchCache(); _patches = new PatchCache();
} }
private static bool StrEquals(string s1, string s2) => string.Equals(s1, s2, StringComparison.OrdinalIgnoreCase); private static bool StrEquals(string s1, string s2) => string.Equals(s1, s2, StringComparison.OrdinalIgnoreCase);
public string GetModsBasePath() => EnsureBaseDirStructure(AppDataManager.GetModsPath()); public static string GetModsBasePath() => EnsureBaseDirStructure(AppDataManager.GetModsPath());
public string GetSdModsBasePath() => EnsureBaseDirStructure(AppDataManager.GetSdModsPath()); public static string GetSdModsBasePath() => EnsureBaseDirStructure(AppDataManager.GetSdModsPath());
private string EnsureBaseDirStructure(string modsBasePath) private static string EnsureBaseDirStructure(string modsBasePath)
{ {
var modsDir = new DirectoryInfo(modsBasePath); var modsDir = new DirectoryInfo(modsBasePath);
modsDir.CreateSubdirectory(AmsContentsDir); modsDir.CreateSubdirectory(AmsContentsDir);
modsDir.CreateSubdirectory(AmsNsoPatchDir); modsDir.CreateSubdirectory(AmsNsoPatchDir);
modsDir.CreateSubdirectory(AmsNroPatchDir); modsDir.CreateSubdirectory(AmsNroPatchDir);
// modsDir.CreateSubdirectory(AmsKipPatchDir); // uncomment when KIPs are supported // TODO: uncomment when KIPs are supported
// modsDir.CreateSubdirectory(AmsKipPatchDir);
return modsDir.FullName; return modsDir.FullName;
} }
private static DirectoryInfo FindTitleDir(DirectoryInfo contentsDir, string titleId) private static DirectoryInfo FindTitleDir(DirectoryInfo contentsDir, string titleId)
=> contentsDir.EnumerateDirectories($"{titleId}*", _dirEnumOptions).FirstOrDefault(); => contentsDir.EnumerateDirectories($"{titleId}*", DirEnumOptions).FirstOrDefault();
public string GetTitleDir(string modsBasePath, string titleId) private static void AddModsFromDirectory(ModCache mods, DirectoryInfo dir, string titleId)
{
System.Text.StringBuilder types = new();
foreach (var modDir in dir.EnumerateDirectories())
{
types.Clear();
Mod<DirectoryInfo> mod = new("", null);
if (StrEquals(RomfsDir, modDir.Name))
{
mods.RomfsDirs.Add(mod = new Mod<DirectoryInfo>($"<{titleId} RomFs>", modDir));
types.Append('R');
}
else if (StrEquals(ExefsDir, modDir.Name))
{
mods.ExefsDirs.Add(mod = new Mod<DirectoryInfo>($"<{titleId} ExeFs>", modDir));
types.Append('E');
}
else if (StrEquals(CheatDir, modDir.Name))
{
types.Append('C', QueryCheatsDir(mods, modDir));
}
else
{
AddModsFromDirectory(mods, modDir, titleId);
}
if (types.Length > 0)
{
Logger.Info?.Print(LogClass.ModLoader, $"Found mod '{mod.Name}' [{types}]");
}
}
}
public static string GetTitleDir(string modsBasePath, string titleId)
{ {
var contentsDir = new DirectoryInfo(Path.Combine(modsBasePath, AmsContentsDir)); var contentsDir = new DirectoryInfo(Path.Combine(modsBasePath, AmsContentsDir));
var titleModsPath = FindTitleDir(contentsDir, titleId); var titleModsPath = FindTitleDir(contentsDir, titleId);
@@ -170,17 +206,32 @@ namespace Ryujinx.HLE.HOS
} }
// Static Query Methods // Static Query Methods
public static void QueryPatchDirs(PatchCache cache, DirectoryInfo patchDir) private static void QueryPatchDirs(PatchCache cache, DirectoryInfo patchDir)
{ {
if (cache.Initialized || !patchDir.Exists) return; if (cache.Initialized || !patchDir.Exists)
{
return;
}
var patches = cache.KipPatches; List<Mod<DirectoryInfo>> patches;
string type = null; string type;
if (StrEquals(AmsNsoPatchDir, patchDir.Name)) { patches = cache.NsoPatches; type = "NSO"; } if (StrEquals(AmsNsoPatchDir, patchDir.Name))
else if (StrEquals(AmsNroPatchDir, patchDir.Name)) { patches = cache.NroPatches; type = "NRO"; } {
else if (StrEquals(AmsKipPatchDir, patchDir.Name)) { patches = cache.KipPatches; type = "KIP"; } patches = cache.NsoPatches; type = "NSO";
else return; }
else if (StrEquals(AmsNroPatchDir, patchDir.Name))
{
patches = cache.NroPatches; type = "NRO";
}
else if (StrEquals(AmsKipPatchDir, patchDir.Name))
{
patches = cache.KipPatches; type = "KIP";
}
else
{
return;
}
foreach (var modDir in patchDir.EnumerateDirectories()) foreach (var modDir in patchDir.EnumerateDirectories())
{ {
@@ -189,9 +240,12 @@ namespace Ryujinx.HLE.HOS
} }
} }
public static void QueryTitleDir(ModCache mods, DirectoryInfo titleDir) private static void QueryTitleDir(ModCache mods, DirectoryInfo titleDir)
{ {
if (!titleDir.Exists) return; if (!titleDir.Exists)
{
return;
}
var fsFile = new FileInfo(Path.Combine(titleDir.FullName, RomfsContainer)); var fsFile = new FileInfo(Path.Combine(titleDir.FullName, RomfsContainer));
if (fsFile.Exists) if (fsFile.Exists)
@@ -205,64 +259,15 @@ namespace Ryujinx.HLE.HOS
mods.ExefsContainers.Add(new Mod<FileInfo>($"<{titleDir.Name} ExeFs>", fsFile)); mods.ExefsContainers.Add(new Mod<FileInfo>($"<{titleDir.Name} ExeFs>", fsFile));
} }
System.Text.StringBuilder types = new System.Text.StringBuilder(5); AddModsFromDirectory(mods, titleDir, titleDir.Name);
foreach (var modDir in titleDir.EnumerateDirectories())
{
types.Clear();
Mod<DirectoryInfo> mod = new Mod<DirectoryInfo>("", null);
if (StrEquals(RomfsDir, modDir.Name))
{
mods.RomfsDirs.Add(mod = new Mod<DirectoryInfo>($"<{titleDir.Name} RomFs>", modDir));
types.Append('R');
}
else if (StrEquals(ExefsDir, modDir.Name))
{
mods.ExefsDirs.Add(mod = new Mod<DirectoryInfo>($"<{titleDir.Name} ExeFs>", modDir));
types.Append('E');
}
else if (StrEquals(CheatDir, modDir.Name))
{
for (int i = 0; i < QueryCheatsDir(mods, modDir); i++)
{
types.Append('C');
}
}
else
{
var romfs = new DirectoryInfo(Path.Combine(modDir.FullName, RomfsDir));
var exefs = new DirectoryInfo(Path.Combine(modDir.FullName, ExefsDir));
var cheat = new DirectoryInfo(Path.Combine(modDir.FullName, CheatDir));
if (romfs.Exists)
{
mods.RomfsDirs.Add(mod = new Mod<DirectoryInfo>(modDir.Name, romfs));
types.Append('R');
}
if (exefs.Exists)
{
mods.ExefsDirs.Add(mod = new Mod<DirectoryInfo>(modDir.Name, exefs));
types.Append('E');
}
if (cheat.Exists)
{
for (int i = 0; i < QueryCheatsDir(mods, cheat); i++)
{
types.Append('C');
}
}
}
if (types.Length > 0) Logger.Info?.Print(LogClass.ModLoader, $"Found mod '{mod.Name}' [{types}]");
}
} }
public static void QueryContentsDir(ModCache mods, DirectoryInfo contentsDir, ulong titleId) public static void QueryContentsDir(ModCache mods, DirectoryInfo contentsDir, ulong titleId)
{ {
if (!contentsDir.Exists) return; if (!contentsDir.Exists)
{
return;
}
Logger.Info?.Print(LogClass.ModLoader, $"Searching mods for {((titleId & 0x1000) != 0 ? "DLC" : "Title")} {titleId:X16}"); Logger.Info?.Print(LogClass.ModLoader, $"Searching mods for {((titleId & 0x1000) != 0 ? "DLC" : "Title")} {titleId:X16}");
@@ -302,9 +307,16 @@ namespace Ryujinx.HLE.HOS
continue; continue;
} }
int oldCheatsCount = mods.Cheats.Count;
// A cheat file can contain several cheats for the same executable, so the file must be parsed in // A cheat file can contain several cheats for the same executable, so the file must be parsed in
// order to properly enumerate them. // order to properly enumerate them.
mods.Cheats.AddRange(GetCheatsInFile(file)); mods.Cheats.AddRange(GetCheatsInFile(file));
if (mods.Cheats.Count - oldCheatsCount > 0)
{
numMods++;
}
} }
return numMods; return numMods;
@@ -313,13 +325,11 @@ namespace Ryujinx.HLE.HOS
private static IEnumerable<Cheat> GetCheatsInFile(FileInfo cheatFile) private static IEnumerable<Cheat> GetCheatsInFile(FileInfo cheatFile)
{ {
string cheatName = DefaultCheatName; string cheatName = DefaultCheatName;
List<string> instructions = new List<string>(); List<string> instructions = new();
List<Cheat> cheats = new List<Cheat>(); List<Cheat> cheats = new();
using (StreamReader cheatData = cheatFile.OpenText()) using StreamReader cheatData = cheatFile.OpenText();
{ while (cheatData.ReadLine() is { } line)
string line;
while ((line = cheatData.ReadLine()) != null)
{ {
line = line.Trim(); line = line.Trim();
@@ -332,18 +342,18 @@ namespace Ryujinx.HLE.HOS
Logger.Warning?.Print(LogClass.ModLoader, $"Ignoring cheat '{cheatFile.FullName}' because it is malformed"); Logger.Warning?.Print(LogClass.ModLoader, $"Ignoring cheat '{cheatFile.FullName}' because it is malformed");
return new List<Cheat>(); return Array.Empty<Cheat>();
} }
// Add the previous section to the list. // Add the previous section to the list.
if (instructions.Count != 0) if (instructions.Count > 0)
{ {
cheats.Add(new Cheat($"<{cheatName} Cheat>", cheatFile, instructions)); cheats.Add(new Cheat($"<{cheatName} Cheat>", cheatFile, instructions));
} }
// Start a new cheat section. // Start a new cheat section.
cheatName = line.Substring(1, line.Length - 2); cheatName = line.Substring(1, line.Length - 2);
instructions = new List<string>(); instructions.Clear();
} }
else if (line.Length > 0) else if (line.Length > 0)
{ {
@@ -353,17 +363,16 @@ namespace Ryujinx.HLE.HOS
} }
// Add the last section being processed. // Add the last section being processed.
if (instructions.Count != 0) if (instructions.Count > 0)
{ {
cheats.Add(new Cheat($"<{cheatName} Cheat>", cheatFile, instructions)); cheats.Add(new Cheat($"<{cheatName} Cheat>", cheatFile, instructions));
} }
}
return cheats; return cheats;
} }
// Assumes searchDirPaths don't overlap // Assumes searchDirPaths don't overlap
public static void CollectMods(Dictionary<ulong, ModCache> modCaches, PatchCache patches, params string[] searchDirPaths) private static void CollectMods(Dictionary<ulong, ModCache> modCaches, PatchCache patches, params string[] searchDirPaths)
{ {
static bool IsPatchesDir(string name) => StrEquals(AmsNsoPatchDir, name) || static bool IsPatchesDir(string name) => StrEquals(AmsNsoPatchDir, name) ||
StrEquals(AmsNroPatchDir, name) || StrEquals(AmsNroPatchDir, name) ||
@@ -375,7 +384,7 @@ namespace Ryujinx.HLE.HOS
{ {
if (IsContentsDir(searchDir.Name)) if (IsContentsDir(searchDir.Name))
{ {
foreach (var (titleId, cache) in modCaches) foreach ((ulong titleId, ModCache cache) in modCaches)
{ {
QueryContentsDir(cache, searchDir, titleId); QueryContentsDir(cache, searchDir, titleId);
} }
@@ -419,15 +428,15 @@ namespace Ryujinx.HLE.HOS
foreach (ulong titleId in titles) foreach (ulong titleId in titles)
{ {
AppMods[titleId] = new ModCache(); _appMods[titleId] = new ModCache();
} }
CollectMods(AppMods, Patches, searchDirPaths); CollectMods(_appMods, _patches, searchDirPaths);
} }
internal IStorage ApplyRomFsMods(ulong titleId, IStorage baseStorage) internal IStorage ApplyRomFsMods(ulong titleId, IStorage baseStorage)
{ {
if (!AppMods.TryGetValue(titleId, out ModCache mods) || mods.RomfsDirs.Count + mods.RomfsContainers.Count == 0) if (!_appMods.TryGetValue(titleId, out ModCache mods) || mods.RomfsDirs.Count + mods.RomfsContainers.Count == 0)
{ {
return baseStorage; return baseStorage;
} }
@@ -487,7 +496,7 @@ namespace Ryujinx.HLE.HOS
return newStorage; return newStorage;
} }
private static void AddFiles(IFileSystem fs, string modName, HashSet<string> fileSet, RomFsBuilder builder) private static void AddFiles(IFileSystem fs, string modName, ISet<string> fileSet, RomFsBuilder builder)
{ {
foreach (var entry in fs.EnumerateEntries() foreach (var entry in fs.EnumerateEntries()
.Where(f => f.Type == DirectoryEntryType.File) .Where(f => f.Type == DirectoryEntryType.File)
@@ -509,7 +518,7 @@ namespace Ryujinx.HLE.HOS
internal bool ReplaceExefsPartition(ulong titleId, ref IFileSystem exefs) internal bool ReplaceExefsPartition(ulong titleId, ref IFileSystem exefs)
{ {
if (!AppMods.TryGetValue(titleId, out ModCache mods) || mods.ExefsContainers.Count == 0) if (!_appMods.TryGetValue(titleId, out ModCache mods) || mods.ExefsContainers.Count == 0)
{ {
return false; return false;
} }
@@ -537,13 +546,13 @@ namespace Ryujinx.HLE.HOS
internal ModLoadResult ApplyExefsMods(ulong titleId, NsoExecutable[] nsos) internal ModLoadResult ApplyExefsMods(ulong titleId, NsoExecutable[] nsos)
{ {
ModLoadResult modLoadResult = new ModLoadResult ModLoadResult modLoadResult = new()
{ {
Stubs = new BitVector32(), Stubs = new BitVector32(),
Replaces = new BitVector32() Replaces = new BitVector32()
}; };
if (!AppMods.TryGetValue(titleId, out ModCache mods) || mods.ExefsDirs.Count == 0) if (!_appMods.TryGetValue(titleId, out ModCache mods) || mods.ExefsDirs.Count == 0)
{ {
return modLoadResult; return modLoadResult;
} }
@@ -561,7 +570,7 @@ namespace Ryujinx.HLE.HOS
{ {
var nsoName = ProcessConst.ExeFsPrefixes[i]; var nsoName = ProcessConst.ExeFsPrefixes[i];
FileInfo nsoFile = new FileInfo(Path.Combine(mod.Path.FullName, nsoName)); FileInfo nsoFile = new(Path.Combine(mod.Path.FullName, nsoName));
if (nsoFile.Exists) if (nsoFile.Exists)
{ {
if (modLoadResult.Replaces[1 << i]) if (modLoadResult.Replaces[1 << i])
@@ -580,7 +589,7 @@ namespace Ryujinx.HLE.HOS
modLoadResult.Stubs[1 << i] |= File.Exists(Path.Combine(mod.Path.FullName, nsoName + StubExtension)); modLoadResult.Stubs[1 << i] |= File.Exists(Path.Combine(mod.Path.FullName, nsoName + StubExtension));
} }
FileInfo npdmFile = new FileInfo(Path.Combine(mod.Path.FullName, "main.npdm")); FileInfo npdmFile = new(Path.Combine(mod.Path.FullName, "main.npdm"));
if (npdmFile.Exists) if (npdmFile.Exists)
{ {
if (modLoadResult.Npdm != null) if (modLoadResult.Npdm != null)
@@ -611,7 +620,7 @@ namespace Ryujinx.HLE.HOS
internal void ApplyNroPatches(NroExecutable nro) internal void ApplyNroPatches(NroExecutable nro)
{ {
var nroPatches = Patches.NroPatches; var nroPatches = _patches.NroPatches;
if (nroPatches.Count == 0) return; if (nroPatches.Count == 0) return;
@@ -622,9 +631,9 @@ namespace Ryujinx.HLE.HOS
internal bool ApplyNsoPatches(ulong titleId, params IExecutable[] programs) internal bool ApplyNsoPatches(ulong titleId, params IExecutable[] programs)
{ {
IEnumerable<Mod<DirectoryInfo>> nsoMods = Patches.NsoPatches; IEnumerable<Mod<DirectoryInfo>> nsoMods = _patches.NsoPatches;
if (AppMods.TryGetValue(titleId, out ModCache mods)) if (_appMods.TryGetValue(titleId, out ModCache mods))
{ {
nsoMods = nsoMods.Concat(mods.ExefsDirs); nsoMods = nsoMods.Concat(mods.ExefsDirs);
} }
@@ -636,7 +645,7 @@ namespace Ryujinx.HLE.HOS
internal void LoadCheats(ulong titleId, ProcessTamperInfo tamperInfo, TamperMachine tamperMachine) internal void LoadCheats(ulong titleId, ProcessTamperInfo tamperInfo, TamperMachine tamperMachine)
{ {
if (tamperInfo == null || tamperInfo.BuildIds == null || tamperInfo.CodeAddresses == null) if (tamperInfo?.BuildIds == null || tamperInfo.CodeAddresses == null)
{ {
Logger.Error?.Print(LogClass.ModLoader, "Unable to install cheat because the associated process is invalid"); Logger.Error?.Print(LogClass.ModLoader, "Unable to install cheat because the associated process is invalid");
@@ -645,14 +654,14 @@ namespace Ryujinx.HLE.HOS
Logger.Info?.Print(LogClass.ModLoader, $"Build ids found for title {titleId:X16}:\n {String.Join("\n ", tamperInfo.BuildIds)}"); Logger.Info?.Print(LogClass.ModLoader, $"Build ids found for title {titleId:X16}:\n {String.Join("\n ", tamperInfo.BuildIds)}");
if (!AppMods.TryGetValue(titleId, out ModCache mods) || mods.Cheats.Count == 0) if (!_appMods.TryGetValue(titleId, out ModCache mods) || mods.Cheats.Count == 0)
{ {
return; return;
} }
var cheats = mods.Cheats; var cheats = mods.Cheats;
var processExes = tamperInfo.BuildIds.Zip(tamperInfo.CodeAddresses, (k, v) => new { k, v }) var processExes = tamperInfo.BuildIds.Zip(tamperInfo.CodeAddresses, (k, v) => new { k, v })
.ToDictionary(x => x.k.Substring(0, Math.Min(Cheat.CheatIdSize, x.k.Length)), x => x.v); .ToDictionary(x => x.k[..Math.Min(Cheat.CheatIdSize, x.k.Length)], x => x.v);
foreach (var cheat in cheats) foreach (var cheat in cheats)
{ {

View File

@@ -2,6 +2,7 @@
using LibHac.FsSystem; using LibHac.FsSystem;
using LibHac.Loader; using LibHac.Loader;
using LibHac.Ns; using LibHac.Ns;
using Ryujinx.HLE.HOS;
using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.HLE.Loaders.Processes.Extensions;
using ApplicationId = LibHac.Ncm.ApplicationId; using ApplicationId = LibHac.Ncm.ApplicationId;
@@ -17,8 +18,8 @@ namespace Ryujinx.HLE.Loaders.Processes
device.Configuration.VirtualFileSystem.ModLoader.CollectMods( device.Configuration.VirtualFileSystem.ModLoader.CollectMods(
new[] { programId }, new[] { programId },
device.Configuration.VirtualFileSystem.ModLoader.GetModsBasePath(), ModLoader.GetModsBasePath(),
device.Configuration.VirtualFileSystem.ModLoader.GetSdModsBasePath()); ModLoader.GetSdModsBasePath());
if (programId != 0) if (programId != 0)
{ {

View File

@@ -8,6 +8,7 @@ using LibHac.Ns;
using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils; using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using ApplicationId = LibHac.Ncm.ApplicationId; using ApplicationId = LibHac.Ncm.ApplicationId;
@@ -35,8 +36,8 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
// Collecting mods related to AocTitleIds and ProgramId. // Collecting mods related to AocTitleIds and ProgramId.
device.Configuration.VirtualFileSystem.ModLoader.CollectMods( device.Configuration.VirtualFileSystem.ModLoader.CollectMods(
device.Configuration.ContentManager.GetAocTitleIds().Prepend(metaLoader.GetProgramId()), device.Configuration.ContentManager.GetAocTitleIds().Prepend(metaLoader.GetProgramId()),
device.Configuration.VirtualFileSystem.ModLoader.GetModsBasePath(), ModLoader.GetModsBasePath(),
device.Configuration.VirtualFileSystem.ModLoader.GetSdModsBasePath()); ModLoader.GetSdModsBasePath());
// Load Nacp file. // Load Nacp file.
var nacpData = new BlitStruct<ApplicationControlProperty>(1); var nacpData = new BlitStruct<ApplicationControlProperty>(1);

View File

@@ -1,9 +0,0 @@
namespace Ryujinx.Headless.SDL2
{
public enum HideCursor
{
Never,
OnIdle,
Always
}
}

View File

@@ -107,8 +107,8 @@ namespace Ryujinx.Headless.SDL2.OpenGL
GraphicsDebugLevel glLogLevel, GraphicsDebugLevel glLogLevel,
AspectRatio aspectRatio, AspectRatio aspectRatio,
bool enableMouse, bool enableMouse,
HideCursor hideCursor) HideCursorMode hideCursorMode)
: base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursor) : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode)
{ {
_glLogLevel = glLogLevel; _glLogLevel = glLogLevel;
} }

View File

@@ -76,8 +76,8 @@ namespace Ryujinx.Headless.SDL2
[Option("enable-mouse", Required = false, Default = false, HelpText = "Enable or disable mouse support.")] [Option("enable-mouse", Required = false, Default = false, HelpText = "Enable or disable mouse support.")]
public bool EnableMouse { get; set; } public bool EnableMouse { get; set; }
[Option("hide-cursor", Required = false, Default = HideCursor.OnIdle, HelpText = "Change when the cursor gets hidden.")] [Option("hide-cursor", Required = false, Default = HideCursorMode.OnIdle, HelpText = "Change when the cursor gets hidden.")]
public HideCursor HideCursor { get; set; } public HideCursorMode HideCursorMode { get; set; }
[Option("list-input-profiles", Required = false, HelpText = "List inputs profiles.")] [Option("list-input-profiles", Required = false, HelpText = "List inputs profiles.")]
public bool ListInputProfiles { get; set; } public bool ListInputProfiles { get; set; }

View File

@@ -478,8 +478,8 @@ namespace Ryujinx.Headless.SDL2
private static WindowBase CreateWindow(Options options) private static WindowBase CreateWindow(Options options)
{ {
return options.GraphicsBackend == GraphicsBackend.Vulkan return options.GraphicsBackend == GraphicsBackend.Vulkan
? new VulkanWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursor) ? new VulkanWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode)
: new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursor); : new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode);
} }
private static IRenderer CreateRenderer(Options options, WindowBase window) private static IRenderer CreateRenderer(Options options, WindowBase window)

View File

@@ -1,4 +1,5 @@
using Ryujinx.Input; using Ryujinx.Common.Configuration;
using Ryujinx.Input;
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Drawing; using System.Drawing;
@@ -13,7 +14,7 @@ namespace Ryujinx.Headless.SDL2
private const int CursorHideIdleTime = 5; // seconds private const int CursorHideIdleTime = 5; // seconds
private bool _isDisposed; private bool _isDisposed;
private HideCursor _hideCursor; private HideCursorMode _hideCursorMode;
private bool _isHidden; private bool _isHidden;
private long _lastCursorMoveTime; private long _lastCursorMoveTime;
@@ -23,12 +24,12 @@ namespace Ryujinx.Headless.SDL2
public Vector2 Scroll { get; private set; } public Vector2 Scroll { get; private set; }
public Size _clientSize; public Size _clientSize;
public SDL2MouseDriver(HideCursor hideCursor) public SDL2MouseDriver(HideCursorMode hideCursorMode)
{ {
PressedButtons = new bool[(int)MouseButton.Count]; PressedButtons = new bool[(int)MouseButton.Count];
_hideCursor = hideCursor; _hideCursorMode = hideCursorMode;
if (_hideCursor == HideCursor.Always) if (_hideCursorMode == HideCursorMode.Always)
{ {
SDL_ShowCursor(SDL_DISABLE); SDL_ShowCursor(SDL_DISABLE);
_isHidden = true; _isHidden = true;
@@ -59,7 +60,7 @@ namespace Ryujinx.Headless.SDL2
private void CheckIdle() private void CheckIdle()
{ {
if (_hideCursor != HideCursor.OnIdle) if (_hideCursorMode != HideCursorMode.OnIdle)
{ {
return; return;
} }

View File

@@ -17,8 +17,8 @@ namespace Ryujinx.Headless.SDL2.Vulkan
GraphicsDebugLevel glLogLevel, GraphicsDebugLevel glLogLevel,
AspectRatio aspectRatio, AspectRatio aspectRatio,
bool enableMouse, bool enableMouse,
HideCursor hideCursor) HideCursorMode hideCursorMode)
: base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursor) : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode)
{ {
_glLogLevel = glLogLevel; _glLogLevel = glLogLevel;
} }

View File

@@ -78,9 +78,9 @@ namespace Ryujinx.Headless.SDL2
GraphicsDebugLevel glLogLevel, GraphicsDebugLevel glLogLevel,
AspectRatio aspectRatio, AspectRatio aspectRatio,
bool enableMouse, bool enableMouse,
HideCursor hideCursor) HideCursorMode hideCursorMode)
{ {
MouseDriver = new SDL2MouseDriver(hideCursor); MouseDriver = new SDL2MouseDriver(hideCursorMode);
_inputManager = inputManager; _inputManager = inputManager;
_inputManager.SetMouseDriver(MouseDriver); _inputManager.SetMouseDriver(MouseDriver);
NpadManager = _inputManager.CreateNpadManager(); NpadManager = _inputManager.CreateNpadManager();

View File

@@ -162,9 +162,9 @@ namespace Ryujinx.Ui.Common.Configuration
public bool ShowConfirmExit { get; set; } public bool ShowConfirmExit { get; set; }
/// <summary> /// <summary>
/// Hide Cursor on Idle /// Whether to hide cursor on idle, always or never
/// </summary> /// </summary>
public bool HideCursorOnIdle { get; set; } public HideCursorMode HideCursor { get; set; }
/// <summary> /// <summary>
/// Enables or disables Vertical Sync /// Enables or disables Vertical Sync

View File

@@ -613,7 +613,7 @@ namespace Ryujinx.Ui.Common.Configuration
/// <summary> /// <summary>
/// Hide Cursor on Idle /// Hide Cursor on Idle
/// </summary> /// </summary>
public ReactiveObject<bool> HideCursorOnIdle { get; private set; } public ReactiveObject<HideCursorMode> HideCursor { get; private set; }
private ConfigurationState() private ConfigurationState()
{ {
@@ -626,7 +626,7 @@ namespace Ryujinx.Ui.Common.Configuration
EnableDiscordIntegration = new ReactiveObject<bool>(); EnableDiscordIntegration = new ReactiveObject<bool>();
CheckUpdatesOnStart = new ReactiveObject<bool>(); CheckUpdatesOnStart = new ReactiveObject<bool>();
ShowConfirmExit = new ReactiveObject<bool>(); ShowConfirmExit = new ReactiveObject<bool>();
HideCursorOnIdle = new ReactiveObject<bool>(); HideCursor = new ReactiveObject<HideCursorMode>();
} }
public ConfigurationFileFormat ToFileFormat() public ConfigurationFileFormat ToFileFormat()
@@ -662,7 +662,7 @@ namespace Ryujinx.Ui.Common.Configuration
EnableDiscordIntegration = EnableDiscordIntegration, EnableDiscordIntegration = EnableDiscordIntegration,
CheckUpdatesOnStart = CheckUpdatesOnStart, CheckUpdatesOnStart = CheckUpdatesOnStart,
ShowConfirmExit = ShowConfirmExit, ShowConfirmExit = ShowConfirmExit,
HideCursorOnIdle = HideCursorOnIdle, HideCursor = HideCursor,
EnableVsync = Graphics.EnableVsync, EnableVsync = Graphics.EnableVsync,
EnableShaderCache = Graphics.EnableShaderCache, EnableShaderCache = Graphics.EnableShaderCache,
EnableTextureRecompression = Graphics.EnableTextureRecompression, EnableTextureRecompression = Graphics.EnableTextureRecompression,
@@ -767,7 +767,7 @@ namespace Ryujinx.Ui.Common.Configuration
EnableDiscordIntegration.Value = true; EnableDiscordIntegration.Value = true;
CheckUpdatesOnStart.Value = true; CheckUpdatesOnStart.Value = true;
ShowConfirmExit.Value = true; ShowConfirmExit.Value = true;
HideCursorOnIdle.Value = false; HideCursor.Value = Ryujinx.Common.Configuration.HideCursorMode.Never;
Graphics.EnableVsync.Value = true; Graphics.EnableVsync.Value = true;
Graphics.EnableShaderCache.Value = true; Graphics.EnableShaderCache.Value = true;
Graphics.EnableTextureRecompression.Value = false; Graphics.EnableTextureRecompression.Value = false;
@@ -1046,7 +1046,7 @@ namespace Ryujinx.Ui.Common.Configuration
{ {
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 22."); Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 22.");
configurationFileFormat.HideCursorOnIdle = false; configurationFileFormat.HideCursor = HideCursorMode.Never;
configurationFileUpdated = true; configurationFileUpdated = true;
} }
@@ -1427,7 +1427,7 @@ namespace Ryujinx.Ui.Common.Configuration
EnableDiscordIntegration.Value = configurationFileFormat.EnableDiscordIntegration; EnableDiscordIntegration.Value = configurationFileFormat.EnableDiscordIntegration;
CheckUpdatesOnStart.Value = configurationFileFormat.CheckUpdatesOnStart; CheckUpdatesOnStart.Value = configurationFileFormat.CheckUpdatesOnStart;
ShowConfirmExit.Value = configurationFileFormat.ShowConfirmExit; ShowConfirmExit.Value = configurationFileFormat.ShowConfirmExit;
HideCursorOnIdle.Value = configurationFileFormat.HideCursorOnIdle; HideCursor.Value = configurationFileFormat.HideCursor;
Graphics.EnableVsync.Value = configurationFileFormat.EnableVsync; Graphics.EnableVsync.Value = configurationFileFormat.EnableVsync;
Graphics.EnableShaderCache.Value = configurationFileFormat.EnableShaderCache; Graphics.EnableShaderCache.Value = configurationFileFormat.EnableShaderCache;
Graphics.EnableTextureRecompression.Value = configurationFileFormat.EnableTextureRecompression; Graphics.EnableTextureRecompression.Value = configurationFileFormat.EnableTextureRecompression;

View File

@@ -9,6 +9,7 @@ namespace Ryujinx.Ui.Common.Helper
public static bool? OverrideDockedMode { get; private set; } public static bool? OverrideDockedMode { get; private set; }
public static string OverrideGraphicsBackend { get; private set; } public static string OverrideGraphicsBackend { get; private set; }
public static string OverrideHideCursor { get; private set; }
public static string BaseDirPathArg { get; private set; } public static string BaseDirPathArg { get; private set; }
public static string Profile { get; private set; } public static string Profile { get; private set; }
public static string LaunchPathArg { get; private set; } public static string LaunchPathArg { get; private set; }
@@ -76,6 +77,16 @@ namespace Ryujinx.Ui.Common.Helper
case "--handheld-mode": case "--handheld-mode":
OverrideDockedMode = false; OverrideDockedMode = false;
break; break;
case "--hide-cursor":
if (i + 1 >= args.Length)
{
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
continue;
}
OverrideHideCursor = args[++i];
break;
default: default:
LaunchPathArg = arg; LaunchPathArg = arg;
break; break;

View File

@@ -209,7 +209,7 @@ namespace Ryujinx
} }
} }
// Check if graphics backend was overridden // Check if graphics backend was overridden.
if (CommandLineState.OverrideGraphicsBackend != null) if (CommandLineState.OverrideGraphicsBackend != null)
{ {
if (CommandLineState.OverrideGraphicsBackend.ToLower() == "opengl") if (CommandLineState.OverrideGraphicsBackend.ToLower() == "opengl")
@@ -224,7 +224,19 @@ namespace Ryujinx
} }
} }
// Check if docked mode was overriden. // Check if HideCursor was overridden.
if (CommandLineState.OverrideHideCursor is not null)
{
ConfigurationState.Instance.HideCursor.Value = CommandLineState.OverrideHideCursor!.ToLower() switch
{
"never" => HideCursorMode.Never,
"onidle" => HideCursorMode.OnIdle,
"always" => HideCursorMode.Always,
_ => ConfigurationState.Instance.HideCursor.Value
};
}
// Check if docked mode was overridden.
if (CommandLineState.OverrideDockedMode.HasValue) if (CommandLineState.OverrideDockedMode.HasValue)
{ {
ConfigurationState.Instance.System.EnableDockedMode.Value = CommandLineState.OverrideDockedMode.Value; ConfigurationState.Instance.System.EnableDockedMode.Value = CommandLineState.OverrideDockedMode.Value;

View File

@@ -72,7 +72,7 @@ namespace Ryujinx.Ui
const int CursorHideIdleTime = 5; // seconds const int CursorHideIdleTime = 5; // seconds
private static readonly Cursor _invisibleCursor = new Cursor(Display.Default, CursorType.BlankCursor); private static readonly Cursor _invisibleCursor = new Cursor(Display.Default, CursorType.BlankCursor);
private long _lastCursorMoveTime; private long _lastCursorMoveTime;
private bool _hideCursorOnIdle; private HideCursorMode _hideCursorMode;
private InputManager _inputManager; private InputManager _inputManager;
private IKeyboard _keyboardInterface; private IKeyboard _keyboardInterface;
private GraphicsDebugLevel _glLogLevel; private GraphicsDebugLevel _glLogLevel;
@@ -113,10 +113,10 @@ namespace Ryujinx.Ui
_gpuCancellationTokenSource = new CancellationTokenSource(); _gpuCancellationTokenSource = new CancellationTokenSource();
_hideCursorOnIdle = ConfigurationState.Instance.HideCursorOnIdle; _hideCursorMode = ConfigurationState.Instance.HideCursor;
_lastCursorMoveTime = Stopwatch.GetTimestamp(); _lastCursorMoveTime = Stopwatch.GetTimestamp();
ConfigurationState.Instance.HideCursorOnIdle.Event += HideCursorStateChanged; ConfigurationState.Instance.HideCursor.Event += HideCursorStateChanged;
ConfigurationState.Instance.Graphics.AntiAliasing.Event += UpdateAnriAliasing; ConfigurationState.Instance.Graphics.AntiAliasing.Event += UpdateAnriAliasing;
ConfigurationState.Instance.Graphics.ScalingFilter.Event += UpdateScalingFilter; ConfigurationState.Instance.Graphics.ScalingFilter.Event += UpdateScalingFilter;
ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel; ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel;
@@ -145,26 +145,32 @@ namespace Ryujinx.Ui
return Renderer.GetHardwareInfo().GpuVendor; return Renderer.GetHardwareInfo().GpuVendor;
} }
private void HideCursorStateChanged(object sender, ReactiveEventArgs<bool> state) private void HideCursorStateChanged(object sender, ReactiveEventArgs<HideCursorMode> state)
{ {
Application.Invoke(delegate Application.Invoke(delegate
{ {
_hideCursorOnIdle = state.NewValue; _hideCursorMode = state.NewValue;
if (_hideCursorOnIdle) switch (_hideCursorMode)
{
_lastCursorMoveTime = Stopwatch.GetTimestamp();
}
else
{ {
case HideCursorMode.Never:
Window.Cursor = null; Window.Cursor = null;
break;
case HideCursorMode.OnIdle:
_lastCursorMoveTime = Stopwatch.GetTimestamp();
break;
case HideCursorMode.Always:
Window.Cursor = _invisibleCursor;
break;
default:
throw new ArgumentOutOfRangeException();
} }
}); });
} }
private void Renderer_Destroyed(object sender, EventArgs e) private void Renderer_Destroyed(object sender, EventArgs e)
{ {
ConfigurationState.Instance.HideCursorOnIdle.Event -= HideCursorStateChanged; ConfigurationState.Instance.HideCursor.Event -= HideCursorStateChanged;
ConfigurationState.Instance.Graphics.AntiAliasing.Event -= UpdateAnriAliasing; ConfigurationState.Instance.Graphics.AntiAliasing.Event -= UpdateAnriAliasing;
ConfigurationState.Instance.Graphics.ScalingFilter.Event -= UpdateScalingFilter; ConfigurationState.Instance.Graphics.ScalingFilter.Event -= UpdateScalingFilter;
ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event -= UpdateScalingFilterLevel; ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event -= UpdateScalingFilterLevel;
@@ -180,7 +186,7 @@ namespace Ryujinx.Ui
protected override bool OnMotionNotifyEvent(EventMotion evnt) protected override bool OnMotionNotifyEvent(EventMotion evnt)
{ {
if (_hideCursorOnIdle) if (_hideCursorMode == HideCursorMode.OnIdle)
{ {
_lastCursorMoveTime = Stopwatch.GetTimestamp(); _lastCursorMoveTime = Stopwatch.GetTimestamp();
} }
@@ -315,17 +321,30 @@ namespace Ryujinx.Ui
_toggleDockedMode = toggleDockedMode; _toggleDockedMode = toggleDockedMode;
if (_hideCursorOnIdle && !ConfigurationState.Instance.Hid.EnableMouse) if (ConfigurationState.Instance.Hid.EnableMouse.Value)
{ {
long cursorMoveDelta = Stopwatch.GetTimestamp() - _lastCursorMoveTime; if (_isMouseInClient)
Window.Cursor = (cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency) ? _invisibleCursor : null;
}
if (ConfigurationState.Instance.Hid.EnableMouse && _isMouseInClient)
{ {
Window.Cursor = _invisibleCursor; Window.Cursor = _invisibleCursor;
} }
} }
else
{
switch (_hideCursorMode)
{
case HideCursorMode.OnIdle:
long cursorMoveDelta = Stopwatch.GetTimestamp() - _lastCursorMoveTime;
Window.Cursor = (cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency) ? _invisibleCursor : null;
break;
case HideCursorMode.Always:
Window.Cursor = _invisibleCursor;
break;
case HideCursorMode.Never:
Window.Cursor = null;
break;
}
}
}
public void Initialize(Switch device) public void Initialize(Switch device)
{ {

View File

@@ -460,16 +460,16 @@ namespace Ryujinx.Ui.Widgets
private void OpenTitleModDir_Clicked(object sender, EventArgs args) private void OpenTitleModDir_Clicked(object sender, EventArgs args)
{ {
string modsBasePath = _virtualFileSystem.ModLoader.GetModsBasePath(); string modsBasePath = ModLoader.GetModsBasePath();
string titleModsPath = _virtualFileSystem.ModLoader.GetTitleDir(modsBasePath, _titleIdText); string titleModsPath = ModLoader.GetTitleDir(modsBasePath, _titleIdText);
OpenHelper.OpenFolder(titleModsPath); OpenHelper.OpenFolder(titleModsPath);
} }
private void OpenTitleSdModDir_Clicked(object sender, EventArgs args) private void OpenTitleSdModDir_Clicked(object sender, EventArgs args)
{ {
string sdModsBasePath = _virtualFileSystem.ModLoader.GetSdModsBasePath(); string sdModsBasePath = ModLoader.GetSdModsBasePath();
string titleModsPath = _virtualFileSystem.ModLoader.GetTitleDir(sdModsBasePath, _titleIdText); string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, _titleIdText);
OpenHelper.OpenFolder(titleModsPath); OpenHelper.OpenFolder(titleModsPath);
} }

View File

@@ -28,8 +28,8 @@ namespace Ryujinx.Ui.Windows
builder.Autoconnect(this); builder.Autoconnect(this);
_baseTitleInfoLabel.Text = $"Cheats Available for {titleName} [{titleId:X16}]"; _baseTitleInfoLabel.Text = $"Cheats Available for {titleName} [{titleId:X16}]";
string modsBasePath = virtualFileSystem.ModLoader.GetModsBasePath(); string modsBasePath = ModLoader.GetModsBasePath();
string titleModsPath = virtualFileSystem.ModLoader.GetTitleDir(modsBasePath, titleId.ToString("X16")); string titleModsPath = ModLoader.GetTitleDir(modsBasePath, titleId.ToString("X16"));
_enabledCheatsPath = System.IO.Path.Combine(titleModsPath, "cheats", "enabled.txt"); _enabledCheatsPath = System.IO.Path.Combine(titleModsPath, "cheats", "enabled.txt");

View File

@@ -52,7 +52,9 @@ namespace Ryujinx.Ui.Windows
[GUI] CheckButton _discordToggle; [GUI] CheckButton _discordToggle;
[GUI] CheckButton _checkUpdatesToggle; [GUI] CheckButton _checkUpdatesToggle;
[GUI] CheckButton _showConfirmExitToggle; [GUI] CheckButton _showConfirmExitToggle;
[GUI] CheckButton _hideCursorOnIdleToggle; [GUI] RadioButton _hideCursorNever;
[GUI] RadioButton _hideCursorOnIdle;
[GUI] RadioButton _hideCursorAlways;
[GUI] CheckButton _vSyncToggle; [GUI] CheckButton _vSyncToggle;
[GUI] CheckButton _shaderCacheToggle; [GUI] CheckButton _shaderCacheToggle;
[GUI] CheckButton _textureRecompressionToggle; [GUI] CheckButton _textureRecompressionToggle;
@@ -226,9 +228,17 @@ namespace Ryujinx.Ui.Windows
_showConfirmExitToggle.Click(); _showConfirmExitToggle.Click();
} }
if (ConfigurationState.Instance.HideCursorOnIdle) switch (ConfigurationState.Instance.HideCursor.Value)
{ {
_hideCursorOnIdleToggle.Click(); case HideCursorMode.Never:
_hideCursorNever.Click();
break;
case HideCursorMode.OnIdle:
_hideCursorOnIdle.Click();
break;
case HideCursorMode.Always:
_hideCursorAlways.Click();
break;
} }
if (ConfigurationState.Instance.Graphics.EnableVsync) if (ConfigurationState.Instance.Graphics.EnableVsync)
@@ -560,6 +570,18 @@ namespace Ryujinx.Ui.Windows
_directoryChanged = false; _directoryChanged = false;
} }
HideCursorMode hideCursor = HideCursorMode.Never;
if (_hideCursorOnIdle.Active)
{
hideCursor = HideCursorMode.OnIdle;
}
if (_hideCursorAlways.Active)
{
hideCursor = HideCursorMode.Always;
}
if (!float.TryParse(_resScaleText.Buffer.Text, out float resScaleCustom) || resScaleCustom <= 0.0f) if (!float.TryParse(_resScaleText.Buffer.Text, out float resScaleCustom) || resScaleCustom <= 0.0f)
{ {
resScaleCustom = 1.0f; resScaleCustom = 1.0f;
@@ -602,7 +624,7 @@ namespace Ryujinx.Ui.Windows
ConfigurationState.Instance.EnableDiscordIntegration.Value = _discordToggle.Active; ConfigurationState.Instance.EnableDiscordIntegration.Value = _discordToggle.Active;
ConfigurationState.Instance.CheckUpdatesOnStart.Value = _checkUpdatesToggle.Active; ConfigurationState.Instance.CheckUpdatesOnStart.Value = _checkUpdatesToggle.Active;
ConfigurationState.Instance.ShowConfirmExit.Value = _showConfirmExitToggle.Active; ConfigurationState.Instance.ShowConfirmExit.Value = _showConfirmExitToggle.Active;
ConfigurationState.Instance.HideCursorOnIdle.Value = _hideCursorOnIdleToggle.Active; ConfigurationState.Instance.HideCursor.Value = hideCursor;
ConfigurationState.Instance.Graphics.EnableVsync.Value = _vSyncToggle.Active; ConfigurationState.Instance.Graphics.EnableVsync.Value = _vSyncToggle.Active;
ConfigurationState.Instance.Graphics.EnableShaderCache.Value = _shaderCacheToggle.Active; ConfigurationState.Instance.Graphics.EnableShaderCache.Value = _shaderCacheToggle.Active;
ConfigurationState.Instance.Graphics.EnableTextureRecompression.Value = _textureRecompressionToggle.Active; ConfigurationState.Instance.Graphics.EnableTextureRecompression.Value = _textureRecompressionToggle.Active;

View File

@@ -160,21 +160,85 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkCheckButton" id="_hideCursorOnIdleToggle"> <object class="GtkBox" id="_hideCursorBox">
<property name="label" translatable="yes">Hide Cursor On Idle</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can-focus">True</property> <property name="can-focus">False</property>
<property name="receives-default">False</property> <child>
<property name="halign">start</property> <object class="GtkLabel">
<property name="draw-indicator">True</property> <property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Hide Cursor:</property>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="padding">5</property> <property name="padding">5</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="_hideCursorNever">
<property name="label" translatable="yes">Never</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="halign">start</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="active">True</property>
<property name="draw-indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property> <property name="position">3</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkRadioButton" id="_hideCursorOnIdle">
<property name="label" translatable="yes">On Idle</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="halign">start</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="draw-indicator">True</property>
<property name="group">_hideCursorNever</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="_hideCursorAlways">
<property name="label" translatable="yes">Always</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="halign">start</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="draw-indicator">True</property>
<property name="group">_hideCursorNever</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">5</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">5</property>
<property name="position">4</property>
</packing>
</child>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>