From 9d28af935d3b22341f9daf9ba51a04cc9610552e Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 28 Jan 2025 20:16:41 -0600 Subject: [PATCH 1/8] headless: Enable Rainbow cycling if any input configs have UseRainbow enabled --- src/Ryujinx.HLE/HLEConfiguration.cs | 1 + src/Ryujinx/Headless/HeadlessRyujinx.cs | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/Ryujinx.HLE/HLEConfiguration.cs b/src/Ryujinx.HLE/HLEConfiguration.cs index 8ac76508f..0b7ae3974 100644 --- a/src/Ryujinx.HLE/HLEConfiguration.cs +++ b/src/Ryujinx.HLE/HLEConfiguration.cs @@ -192,6 +192,7 @@ namespace Ryujinx.HLE /// /// The desired hacky workarounds. /// + /// This cannot be changed after instantiation. public EnabledDirtyHack[] Hacks { internal get; set; } public HLEConfiguration(VirtualFileSystem virtualFileSystem, diff --git a/src/Ryujinx/Headless/HeadlessRyujinx.cs b/src/Ryujinx/Headless/HeadlessRyujinx.cs index 18efdceee..fafcbf01e 100644 --- a/src/Ryujinx/Headless/HeadlessRyujinx.cs +++ b/src/Ryujinx/Headless/HeadlessRyujinx.cs @@ -5,6 +5,7 @@ using Ryujinx.Ava.Utilities.Configuration; using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Hid.Controller; using Ryujinx.Common.GraphicsDriver; using Ryujinx.Common.Logging; using Ryujinx.Common.Logging.Targets; @@ -26,6 +27,7 @@ using Ryujinx.SDL2.Common; using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading; namespace Ryujinx.Headless @@ -286,6 +288,9 @@ namespace Ryujinx.Headless GraphicsConfig.EnableMacroHLE = !option.DisableMacroHLE; DriverUtilities.InitDriverConfig(option.BackendThreading == BackendThreading.Off); + + if (_inputConfiguration.OfType().Any(ic => ic.Led.UseRainbow)) + Rainbow.Enable(); while (true) { From 7085bafa60272a0cbb707c5b4b96abcddec78bd0 Mon Sep 17 00:00:00 2001 From: LotP1 <68976644+LotP1@users.noreply.github.com> Date: Wed, 29 Jan 2025 03:36:58 +0100 Subject: [PATCH 2/8] PPTC Profiles (#370) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added functionality that allows ExeFS mods to compile to their own PPTC Profile and therefore store PTC data between sessions. The feature calculates the hash of the currently loaded ExeFS mods and stores the PPTC data in a profile that matches said hash, so you can have multiple ExeFS loadouts without causing issues. This includes different versions of the same mod as their hashes will be different. Using this PR should be seamless as the JIT Sparse PR already laid the groundwork for PPTC Profiles and this PR just allows ExeFS mods to load and store their own profiles besides the `default` profile. ❗❗❗ **WARNING!** ❗❗❗ **This will update your PPTC profile version, which means the PPTC profile will be invalidated if you try to run a PR/Build/Branch that does not include this change!** **This is only relevant for the default PPTC Profile, as any other profiles do not exist to older versions!** --- src/ARMeilleure/Translation/PTC/Ptc.cs | 56 ++++++++++-- .../Translation/PTC/PtcProfiler.cs | 90 ++++++++++++++++--- src/ARMeilleure/Translation/Translator.cs | 10 +++ .../HOS/ArmProcessContextFactory.cs | 5 +- src/Ryujinx.HLE/HOS/ModLoader.cs | 26 +++++- .../Extensions/FileSystemExtensions.cs | 10 +-- .../Loaders/Processes/ProcessLoader.cs | 1 + .../Loaders/Processes/ProcessLoaderHelper.cs | 3 + src/Ryujinx/Assets/locales.json | 75 ++++++++++++++++ .../UI/Controls/ApplicationContextMenu.axaml | 5 ++ .../Controls/ApplicationContextMenu.axaml.cs | 46 ++++++++++ 11 files changed, 299 insertions(+), 28 deletions(-) diff --git a/src/ARMeilleure/Translation/PTC/Ptc.cs b/src/ARMeilleure/Translation/PTC/Ptc.cs index b53fdd4df..d1ffda830 100644 --- a/src/ARMeilleure/Translation/PTC/Ptc.cs +++ b/src/ARMeilleure/Translation/PTC/Ptc.cs @@ -3,6 +3,7 @@ using ARMeilleure.CodeGen.Linking; using ARMeilleure.CodeGen.Unwinding; using ARMeilleure.Common; using ARMeilleure.Memory; +using ARMeilleure.State; using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; @@ -30,8 +31,8 @@ namespace ARMeilleure.Translation.PTC { private const string OuterHeaderMagicString = "PTCohd\0\0"; private const string InnerHeaderMagicString = "PTCihd\0\0"; - - private const uint InternalVersion = 6998; //! To be incremented manually for each change to the ARMeilleure project. + + private const uint InternalVersion = 7007; //! To be incremented manually for each change to the ARMeilleure project. private const string ActualDir = "0"; private const string BackupDir = "1"; @@ -184,6 +185,36 @@ namespace ARMeilleure.Translation.PTC InitializeCarriers(); } + private bool ContainsBlacklistedFunctions() + { + List blacklist = Profiler.GetBlacklistedFunctions(); + bool containsBlacklistedFunctions = false; + _infosStream.Seek(0L, SeekOrigin.Begin); + bool foundBadFunction = false; + + for (int index = 0; index < GetEntriesCount(); index++) + { + InfoEntry infoEntry = DeserializeStructure(_infosStream); + foreach (ulong address in blacklist) + { + if (infoEntry.Address == address) + { + containsBlacklistedFunctions = true; + Logger.Warning?.Print(LogClass.Ptc, "PPTC cache invalidated: Found blacklisted functions in PPTC cache"); + foundBadFunction = true; + break; + } + } + + if (foundBadFunction) + { + break; + } + } + + return containsBlacklistedFunctions; + } + private void PreLoad() { string fileNameActual = $"{CachePathActual}.cache"; @@ -532,7 +563,7 @@ namespace ARMeilleure.Translation.PTC public void LoadTranslations(Translator translator) { - if (AreCarriersEmpty()) + if (AreCarriersEmpty() || ContainsBlacklistedFunctions()) { return; } @@ -835,10 +866,18 @@ namespace ARMeilleure.Translation.PTC while (profiledFuncsToTranslate.TryDequeue(out (ulong address, PtcProfiler.FuncProfile funcProfile) item)) { ulong address = item.address; + ExecutionMode executionMode = item.funcProfile.Mode; + bool highCq = item.funcProfile.HighCq; Debug.Assert(Profiler.IsAddressInStaticCodeRange(address)); - TranslatedFunction func = translator.Translate(address, item.funcProfile.Mode, item.funcProfile.HighCq); + TranslatedFunction func = translator.Translate(address, executionMode, highCq); + + if (func == null) + { + Profiler.UpdateEntry(address, executionMode, true, true); + continue; + } bool isAddressUnique = translator.Functions.TryAdd(address, func.GuestSize, func); @@ -885,7 +924,14 @@ namespace ARMeilleure.Translation.PTC PtcStateChanged?.Invoke(PtcLoadingState.Loaded, _translateCount, _translateTotalCount); - Logger.Info?.Print(LogClass.Ptc, $"{_translateCount} of {_translateTotalCount} functions translated | Thread count: {degreeOfParallelism} in {sw.Elapsed.TotalSeconds} s"); + if (_translateCount == _translateTotalCount) + { + Logger.Info?.Print(LogClass.Ptc, $"{_translateCount} of {_translateTotalCount} functions translated | Thread count: {degreeOfParallelism} in {sw.Elapsed.TotalSeconds} s"); + } + else + { + Logger.Info?.Print(LogClass.Ptc, $"{_translateCount} of {_translateTotalCount} functions translated | {_translateTotalCount - _translateCount} function{(_translateTotalCount - _translateCount != 1 ? "s" : "")} blacklisted | Thread count: {degreeOfParallelism} in {sw.Elapsed.TotalSeconds} s"); + } Thread preSaveThread = new(PreSave) { diff --git a/src/ARMeilleure/Translation/PTC/PtcProfiler.cs b/src/ARMeilleure/Translation/PTC/PtcProfiler.cs index 21987f72d..de0b78dbe 100644 --- a/src/ARMeilleure/Translation/PTC/PtcProfiler.cs +++ b/src/ARMeilleure/Translation/PTC/PtcProfiler.cs @@ -24,11 +24,12 @@ namespace ARMeilleure.Translation.PTC { private const string OuterHeaderMagicString = "Pohd\0\0\0\0"; - private const uint InternalVersion = 5518; //! Not to be incremented manually for each change to the ARMeilleure project. + private const uint InternalVersion = 7007; //! Not to be incremented manually for each change to the ARMeilleure project. - private static readonly uint[] _migrateInternalVersions = + private static readonly uint[] _migrateInternalVersions = [ - 1866 + 1866, + 5518, ]; private const int SaveInterval = 30; // Seconds. @@ -77,20 +78,30 @@ namespace ARMeilleure.Translation.PTC private void TimerElapsed(object _, ElapsedEventArgs __) => new Thread(PreSave) { Name = "Ptc.DiskWriter" }.Start(); - public void AddEntry(ulong address, ExecutionMode mode, bool highCq) + public void AddEntry(ulong address, ExecutionMode mode, bool highCq, bool blacklist = false) { if (IsAddressInStaticCodeRange(address)) { Debug.Assert(!highCq); - lock (_lock) + if (blacklist) { - ProfiledFuncs.TryAdd(address, new FuncProfile(mode, highCq: false)); + lock (_lock) + { + ProfiledFuncs[address] = new FuncProfile(mode, highCq: false, true); + } + } + else + { + lock (_lock) + { + ProfiledFuncs.TryAdd(address, new FuncProfile(mode, highCq: false, false)); + } } } } - public void UpdateEntry(ulong address, ExecutionMode mode, bool highCq) + public void UpdateEntry(ulong address, ExecutionMode mode, bool highCq, bool? blacklist = null) { if (IsAddressInStaticCodeRange(address)) { @@ -100,7 +111,7 @@ namespace ARMeilleure.Translation.PTC { Debug.Assert(ProfiledFuncs.ContainsKey(address)); - ProfiledFuncs[address] = new FuncProfile(mode, highCq: true); + ProfiledFuncs[address] = new FuncProfile(mode, highCq: true, blacklist ?? ProfiledFuncs[address].Blacklist); } } } @@ -116,7 +127,7 @@ namespace ARMeilleure.Translation.PTC foreach (KeyValuePair profiledFunc in ProfiledFuncs) { - if (!funcs.ContainsKey(profiledFunc.Key)) + if (!funcs.ContainsKey(profiledFunc.Key) && !profiledFunc.Value.Blacklist) { profiledFuncsToTranslate.Enqueue((profiledFunc.Key, profiledFunc.Value)); } @@ -131,6 +142,24 @@ namespace ARMeilleure.Translation.PTC ProfiledFuncs.TrimExcess(); } + public List GetBlacklistedFunctions() + { + List funcs = new List(); + + foreach (var profiledFunc in ProfiledFuncs) + { + if (profiledFunc.Value.Blacklist) + { + if (!funcs.Contains(profiledFunc.Key)) + { + funcs.Add(profiledFunc.Key); + } + } + } + + return funcs; + } + public void PreLoad() { _lastHash = default; @@ -221,13 +250,18 @@ namespace ARMeilleure.Translation.PTC return false; } + Func migrateEntryFunc = null; + switch (outerHeader.InfoFileVersion) { case InternalVersion: ProfiledFuncs = Deserialize(stream); break; case 1866: - ProfiledFuncs = Deserialize(stream, (address, profile) => (address + 0x500000UL, profile)); + migrateEntryFunc = (address, profile) => (address + 0x500000UL, profile); + goto case 5518; + case 5518: + ProfiledFuncs = DeserializeAddBlacklist(stream, migrateEntryFunc); break; default: Logger.Error?.Print(LogClass.Ptc, $"No migration path for {nameof(outerHeader.InfoFileVersion)} '{outerHeader.InfoFileVersion}'. Discarding cache."); @@ -257,6 +291,16 @@ namespace ARMeilleure.Translation.PTC return DeserializeDictionary(stream, DeserializeStructure); } + private static Dictionary DeserializeAddBlacklist(Stream stream, Func migrateEntryFunc = null) + { + if (migrateEntryFunc != null) + { + return DeserializeAndUpdateDictionary(stream, (Stream stream) => { return new FuncProfile(DeserializeStructure(stream)); }, migrateEntryFunc); + } + + return DeserializeDictionary(stream, (Stream stream) => { return new FuncProfile(DeserializeStructure(stream)); }); + } + private static ReadOnlySpan GetReadOnlySpan(MemoryStream memoryStream) { return new(memoryStream.GetBuffer(), (int)memoryStream.Position, (int)memoryStream.Length - (int)memoryStream.Position); @@ -388,13 +432,35 @@ namespace ARMeilleure.Translation.PTC } } - [StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 5*/)] + [StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 6*/)] public struct FuncProfile { public ExecutionMode Mode; public bool HighCq; + public bool Blacklist; - public FuncProfile(ExecutionMode mode, bool highCq) + public FuncProfile(ExecutionMode mode, bool highCq, bool blacklist) + { + Mode = mode; + HighCq = highCq; + Blacklist = blacklist; + } + + public FuncProfile(FuncProfilePreBlacklist fp) + { + Mode = fp.Mode; + HighCq = fp.HighCq; + Blacklist = false; + } + } + + [StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 5*/)] + public struct FuncProfilePreBlacklist + { + public ExecutionMode Mode; + public bool HighCq; + + public FuncProfilePreBlacklist(ExecutionMode mode, bool highCq) { Mode = mode; HighCq = highCq; diff --git a/src/ARMeilleure/Translation/Translator.cs b/src/ARMeilleure/Translation/Translator.cs index 0f18c8045..a50702add 100644 --- a/src/ARMeilleure/Translation/Translator.cs +++ b/src/ARMeilleure/Translation/Translator.cs @@ -249,6 +249,11 @@ namespace ARMeilleure.Translation ControlFlowGraph cfg = EmitAndGetCFG(context, blocks, out Range funcRange, out Counter counter); + if (cfg == null) + { + return null; + } + ulong funcSize = funcRange.End - funcRange.Start; Logger.EndPass(PassName.Translation, cfg); @@ -407,6 +412,11 @@ namespace ARMeilleure.Translation if (opCode.Instruction.Emitter != null) { opCode.Instruction.Emitter(context); + if (opCode.Instruction.Name == InstName.Und && blkIndex == 0) + { + range = new Range(rangeStart, rangeEnd); + return null; + } } else { diff --git a/src/Ryujinx.HLE/HOS/ArmProcessContextFactory.cs b/src/Ryujinx.HLE/HOS/ArmProcessContextFactory.cs index 95b6167f3..08d929bf0 100644 --- a/src/Ryujinx.HLE/HOS/ArmProcessContextFactory.cs +++ b/src/Ryujinx.HLE/HOS/ArmProcessContextFactory.cs @@ -20,6 +20,7 @@ namespace Ryujinx.HLE.HOS private readonly string _titleIdText; private readonly string _displayVersion; private readonly bool _diskCacheEnabled; + private readonly string _diskCacheSelector; private readonly ulong _codeAddress; private readonly ulong _codeSize; @@ -31,6 +32,7 @@ namespace Ryujinx.HLE.HOS string titleIdText, string displayVersion, bool diskCacheEnabled, + string diskCacheSelector, ulong codeAddress, ulong codeSize) { @@ -39,6 +41,7 @@ namespace Ryujinx.HLE.HOS _titleIdText = titleIdText; _displayVersion = displayVersion; _diskCacheEnabled = diskCacheEnabled; + _diskCacheSelector = diskCacheSelector; _codeAddress = codeAddress; _codeSize = codeSize; } @@ -114,7 +117,7 @@ namespace Ryujinx.HLE.HOS } } - DiskCacheLoadState = processContext.Initialize(_titleIdText, _displayVersion, _diskCacheEnabled, _codeAddress, _codeSize, "default"); //Ready for exefs profiles + DiskCacheLoadState = processContext.Initialize(_titleIdText, _displayVersion, _diskCacheEnabled, _codeAddress, _codeSize, _diskCacheSelector ?? "default"); return processContext; } diff --git a/src/Ryujinx.HLE/HOS/ModLoader.cs b/src/Ryujinx.HLE/HOS/ModLoader.cs index ff691914c..6c97e6fed 100644 --- a/src/Ryujinx.HLE/HOS/ModLoader.cs +++ b/src/Ryujinx.HLE/HOS/ModLoader.cs @@ -6,6 +6,7 @@ using LibHac.Loader; using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.RomFs; +using LibHac.Util; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; @@ -19,6 +20,7 @@ using System.Collections.Specialized; using System.Globalization; using System.IO; using System.Linq; +using System.Security.Cryptography; using LazyFile = Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy.LazyFile; using Path = System.IO.Path; @@ -581,6 +583,7 @@ namespace Ryujinx.HLE.HOS public BitVector32 Stubs; public BitVector32 Replaces; public MetaLoader Npdm; + public string Hash; public bool Modified => (Stubs.Data | Replaces.Data) != 0; } @@ -591,8 +594,11 @@ namespace Ryujinx.HLE.HOS { Stubs = new BitVector32(), Replaces = new BitVector32(), + Hash = null, }; + string tempHash = string.Empty; + if (!_appMods.TryGetValue(applicationId, out ModCache mods) || mods.ExefsDirs.Count == 0) { return modLoadResult; @@ -628,8 +634,16 @@ namespace Ryujinx.HLE.HOS modLoadResult.Replaces[1 << i] = true; - nsos[i] = new NsoExecutable(nsoFile.OpenRead().AsStorage(), nsoName); - Logger.Info?.Print(LogClass.ModLoader, $"NSO '{nsoName}' replaced"); + using (FileStream stream = nsoFile.OpenRead()) + { + nsos[i] = new NsoExecutable(stream.AsStorage(), nsoName); + Logger.Info?.Print(LogClass.ModLoader, $"NSO '{nsoName}' replaced"); + using (MD5 md5 = MD5.Create()) + { + stream.Seek(0, SeekOrigin.Begin); + tempHash += BitConverter.ToString(md5.ComputeHash(stream)).Replace("-", "").ToLowerInvariant(); + } + } } modLoadResult.Stubs[1 << i] |= File.Exists(Path.Combine(mod.Path.FullName, nsoName + StubExtension)); @@ -661,6 +675,14 @@ namespace Ryujinx.HLE.HOS } } + if (!string.IsNullOrEmpty(tempHash)) + { + using (MD5 md5 = MD5.Create()) + { + modLoadResult.Hash += BitConverter.ToString(md5.ComputeHash(tempHash.ToBytes())).Replace("-", "").ToLowerInvariant(); + } + } + return modLoadResult; } diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs index 5874636e7..01f65206f 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs @@ -84,13 +84,6 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions // Apply Nsos patches. device.Configuration.VirtualFileSystem.ModLoader.ApplyNsoPatches(programId, nsoExecutables); - // Don't use PTC if ExeFS files have been replaced. - bool enablePtc = device.System.EnablePtc && !modLoadResult.Modified; - if (!enablePtc) - { - Logger.Warning?.Print(LogClass.Ptc, "Detected unsupported ExeFs modifications. PTC disabled."); - } - string programName = string.Empty; if (!isHomebrew && programId > 0x010000000000FFFF) @@ -117,7 +110,8 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions device.System.KernelContext, metaLoader, nacpData, - enablePtc, + device.System.EnablePtc, + modLoadResult.Hash, true, programName, metaLoader.GetProgramId(), diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs index 726b017b6..4c0866531 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs @@ -235,6 +235,7 @@ namespace Ryujinx.HLE.Loaders.Processes dummyExeFs.GetNpdm(), nacpData, diskCacheEnabled: false, + diskCacheSelector: null, allowCodeMemoryForJit: true, programName, programId, diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs index b11057da2..badced1b9 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs @@ -186,6 +186,7 @@ namespace Ryujinx.HLE.Loaders.Processes string.Empty, string.Empty, false, + null, codeAddress, codeSize); @@ -226,6 +227,7 @@ namespace Ryujinx.HLE.Loaders.Processes MetaLoader metaLoader, BlitStruct applicationControlProperties, bool diskCacheEnabled, + string diskCacheSelector, bool allowCodeMemoryForJit, string name, ulong programId, @@ -379,6 +381,7 @@ namespace Ryujinx.HLE.Loaders.Processes $"{programId:x16}", displayVersion, diskCacheEnabled, + diskCacheSelector, codeStart, codeSize); diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index d96682956..ed1f8a1f0 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -2022,6 +2022,56 @@ "zh_TW": "下一次啟動遊戲時,觸發 PPTC 進行重建" } }, + { + "ID": "GameListContextMenuCacheManagementNukePptc", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Purge PPTC cache", + "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": "GameListContextMenuCacheManagementNukePptcToolTip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Deletes all PPTC cache files for the Application", + "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": "GameListContextMenuCacheManagementPurgeShaderCache", "Translations": { @@ -12947,6 +12997,31 @@ "zh_TW": "在 {0} 清除 PPTC 快取時出錯: {1}" } }, + { + "ID": "DialogPPTCNukeMessage", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "You are about to purge all PPTC data from:\n\n{0}\n\nAre you sure you want to proceed?", + "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": "DialogShaderDeletionMessage", "Translations": { diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml index 9fed95aa7..475b26787 100644 --- a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml @@ -81,6 +81,11 @@ Header="{ext:Locale GameListContextMenuCacheManagementPurgePptc}" Icon="{ext:Icon mdi-refresh}" ToolTip.Tip="{ext:Locale GameListContextMenuCacheManagementPurgePptcToolTip}" /> + cacheFiles = new(); + + if (mainDir.Exists) + { + cacheFiles.AddRange(mainDir.EnumerateFiles("*.cache")); + cacheFiles.AddRange(mainDir.EnumerateFiles("*.info")); + } + + if (backupDir.Exists) + { + cacheFiles.AddRange(backupDir.EnumerateFiles("*.cache")); + cacheFiles.AddRange(mainDir.EnumerateFiles("*.info")); + } + + if (cacheFiles.Count > 0) + { + foreach (FileInfo file in cacheFiles) + { + try + { + file.Delete(); + } + catch (Exception ex) + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionErrorMessage, file.Name, ex)); + } + } + } + } + } + public async void PurgeShaderCache_Click(object sender, RoutedEventArgs args) { if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) From 502ce98b3a313c718b3f6ca880bf5efc705e1783 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 28 Jan 2025 21:18:49 -0600 Subject: [PATCH 3/8] UI: [ci skip] Make cheat window larger by default --- src/Ryujinx/UI/Models/CheatNode.cs | 2 +- src/Ryujinx/UI/Windows/CheatWindow.axaml | 4 ++-- src/Ryujinx/UI/Windows/CheatWindow.axaml.cs | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Ryujinx/UI/Models/CheatNode.cs b/src/Ryujinx/UI/Models/CheatNode.cs index 4fc249e20..b036ba6b9 100644 --- a/src/Ryujinx/UI/Models/CheatNode.cs +++ b/src/Ryujinx/UI/Models/CheatNode.cs @@ -9,7 +9,7 @@ namespace Ryujinx.Ava.UI.Models { private bool _isEnabled = false; public ObservableCollection SubNodes { get; } = []; - public string CleanName => Name[1..^7]; + public string CleanName => Name.Length > 0 ? Name[1..^7] : Name; public string BuildIdKey => $"{BuildId}-{Name}"; public bool IsRootNode { get; } public string Name { get; } diff --git a/src/Ryujinx/UI/Windows/CheatWindow.axaml b/src/Ryujinx/UI/Windows/CheatWindow.axaml index c6d485c9b..32f914019 100644 --- a/src/Ryujinx/UI/Windows/CheatWindow.axaml +++ b/src/Ryujinx/UI/Windows/CheatWindow.axaml @@ -6,8 +6,8 @@ xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows" - Width="500" - Height="500" + Width="600" + Height="750" MinWidth="500" MinHeight="500" x:DataType="window:CheatWindow" diff --git a/src/Ryujinx/UI/Windows/CheatWindow.axaml.cs b/src/Ryujinx/UI/Windows/CheatWindow.axaml.cs index ba384359a..e0ba9e419 100644 --- a/src/Ryujinx/UI/Windows/CheatWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/CheatWindow.axaml.cs @@ -34,6 +34,9 @@ namespace Ryujinx.Ava.UI.Windows public CheatWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName, string titlePath) { + MinWidth = 500; + MinHeight = 650; + LoadedCheats = []; IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid From 1b3656bca98a7db08868179afcaf07093548724d Mon Sep 17 00:00:00 2001 From: shinyoyo Date: Wed, 29 Jan 2025 11:29:06 +0800 Subject: [PATCH 4/8] LED Color & LED settings header (zh_CN) (#590) --- src/Ryujinx/Assets/locales.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index ed1f8a1f0..4706abc68 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -7768,7 +7768,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "颜色", "zh_TW": "" } }, @@ -19018,7 +19018,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "LED 设置", "zh_TW": "" } }, From a469f3d710787d535b5efbd762ee32c7603ec6e7 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 28 Jan 2025 21:47:29 -0600 Subject: [PATCH 5/8] UI: Remove empty StackPanel in UserSelectorDialog --- src/Ryujinx/UI/Applet/UserSelectorDialog.axaml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Ryujinx/UI/Applet/UserSelectorDialog.axaml b/src/Ryujinx/UI/Applet/UserSelectorDialog.axaml index ed22fb088..626ad2e21 100644 --- a/src/Ryujinx/UI/Applet/UserSelectorDialog.axaml +++ b/src/Ryujinx/UI/Applet/UserSelectorDialog.axaml @@ -110,12 +110,5 @@ - - From 191e1582897e4485c5d621efca3006fb58a06883 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 28 Jan 2025 22:11:48 -0600 Subject: [PATCH 6/8] misc: chore: Use static instances of converters instead of using control resources --- .../UI/Applet/UserSelectorDialog.axaml | 7 +-- .../UI/Controls/ApplicationGridView.axaml | 5 +- .../UI/Controls/ApplicationListView.axaml | 5 +- .../Converters/BitmapArrayValueConverter.cs | 2 +- .../DownloadableContentLabelConverter.cs | 2 +- .../Helpers/Converters/KeyValueConverter.cs | 2 +- .../Converters/MultiplayerInfoConverter.cs | 12 +--- .../Converters/TitleUpdateLabelConverter.cs | 2 +- .../XCITrimmerFileSpaceSavingsConverter.cs | 2 +- .../UI/Views/Input/ControllerInputView.axaml | 47 +++++++-------- .../UI/Views/Input/KeyboardInputView.axaml | 59 +++++++++---------- .../Views/Settings/SettingsHotkeysView.axaml | 25 ++++---- .../Views/Settings/SettingsSystemView.axaml | 5 +- .../UI/Views/User/UserEditorView.axaml | 5 +- .../User/UserFirmwareAvatarSelectorView.axaml | 5 +- .../UI/Views/User/UserSaveManagerView.axaml | 5 +- .../UI/Views/User/UserSelectorView.axaml | 5 +- src/Ryujinx/UI/Windows/MainWindow.axaml | 5 +- .../UI/Windows/TitleUpdateWindow.axaml | 5 +- src/Ryujinx/UI/Windows/XCITrimmerWindow.axaml | 11 +--- 20 files changed, 82 insertions(+), 134 deletions(-) diff --git a/src/Ryujinx/UI/Applet/UserSelectorDialog.axaml b/src/Ryujinx/UI/Applet/UserSelectorDialog.axaml index 626ad2e21..2816afbce 100644 --- a/src/Ryujinx/UI/Applet/UserSelectorDialog.axaml +++ b/src/Ryujinx/UI/Applet/UserSelectorDialog.axaml @@ -13,11 +13,6 @@ mc:Ignorable="d" Focusable="True" x:DataType="viewModels:UserSelectorDialogViewModel"> - - - - - @@ -80,7 +75,7 @@ Height="96" HorizontalAlignment="Stretch" VerticalAlignment="Top" - Source="{Binding Image, Converter={StaticResource ByteImage}}" /> + Source="{Binding Image, Converter={x:Static helpers:BitmapArrayValueConverter.Instance}}" /> - - - @@ -68,7 +65,7 @@ Grid.Row="0" HorizontalAlignment="Stretch" VerticalAlignment="Top" - Source="{Binding Icon, Converter={StaticResource ByteImage}}" /> + Source="{Binding Icon, Converter={x:Static helpers:BitmapArrayValueConverter.Instance}}" /> - - - @@ -62,7 +59,7 @@ Classes.large="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridLarge}" Classes.normal="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridMedium}" Classes.small="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridSmall}" - Source="{Binding Icon, Converter={StaticResource ByteImage}}" /> + Source="{Binding Icon, Converter={x:Static helpers:BitmapArrayValueConverter.Instance}}" /> values, Type targetType, object parameter, CultureInfo culture) { diff --git a/src/Ryujinx/UI/Helpers/Converters/KeyValueConverter.cs b/src/Ryujinx/UI/Helpers/Converters/KeyValueConverter.cs index d20098426..e7a157892 100644 --- a/src/Ryujinx/UI/Helpers/Converters/KeyValueConverter.cs +++ b/src/Ryujinx/UI/Helpers/Converters/KeyValueConverter.cs @@ -10,7 +10,7 @@ namespace Ryujinx.Ava.UI.Helpers { internal class KeyValueConverter : IValueConverter { - public static KeyValueConverter Instance = new(); + public static readonly KeyValueConverter Instance = new(); private static readonly Dictionary _keysMap = new() { diff --git a/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs b/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs index 09a2ad367..47d0b94d0 100644 --- a/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs +++ b/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs @@ -1,6 +1,5 @@ using Avalonia.Data.Converters; using Avalonia.Markup.Xaml; -using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Utilities.AppLibrary; using System; using System.Globalization; @@ -19,15 +18,10 @@ namespace Ryujinx.Ava.UI.Helpers { return $"Hosted Games: {applicationData.GameCount}\nOnline Players: {applicationData.PlayerCount}"; } - else - { - return ""; - } - } - else - { - return ""; } + + return ""; + } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) diff --git a/src/Ryujinx/UI/Helpers/Converters/TitleUpdateLabelConverter.cs b/src/Ryujinx/UI/Helpers/Converters/TitleUpdateLabelConverter.cs index d462b9463..f1e6b8958 100644 --- a/src/Ryujinx/UI/Helpers/Converters/TitleUpdateLabelConverter.cs +++ b/src/Ryujinx/UI/Helpers/Converters/TitleUpdateLabelConverter.cs @@ -11,7 +11,7 @@ namespace Ryujinx.Ava.UI.Helpers { internal class TitleUpdateLabelConverter : IMultiValueConverter { - public static TitleUpdateLabelConverter Instance = new(); + public static readonly TitleUpdateLabelConverter Instance = new(); public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) { diff --git a/src/Ryujinx/UI/Helpers/Converters/XCITrimmerFileSpaceSavingsConverter.cs b/src/Ryujinx/UI/Helpers/Converters/XCITrimmerFileSpaceSavingsConverter.cs index ab6199c3f..d70a795c0 100644 --- a/src/Ryujinx/UI/Helpers/Converters/XCITrimmerFileSpaceSavingsConverter.cs +++ b/src/Ryujinx/UI/Helpers/Converters/XCITrimmerFileSpaceSavingsConverter.cs @@ -12,7 +12,7 @@ namespace Ryujinx.Ava.UI.Helpers internal class XCITrimmerFileSpaceSavingsConverter : IValueConverter { private const long _bytesPerMB = 1024 * 1024; - public static XCITrimmerFileSpaceSavingsConverter Instance = new(); + public static readonly XCITrimmerFileSpaceSavingsConverter Instance = new(); public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { diff --git a/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml b/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml index 5cf0fa03a..49c2cfd4c 100644 --- a/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml +++ b/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml @@ -20,9 +20,6 @@ - - -