diff --git a/Directory.Packages.props b/Directory.Packages.props index 203f40588..a480d3d29 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -42,7 +42,7 @@ - + diff --git a/src/Ryujinx.HLE/HOS/Horizon.cs b/src/Ryujinx.HLE/HOS/Horizon.cs index f9c5ddecf..627b649df 100644 --- a/src/Ryujinx.HLE/HOS/Horizon.cs +++ b/src/Ryujinx.HLE/HOS/Horizon.cs @@ -284,7 +284,7 @@ namespace Ryujinx.HLE.HOS ProcessCreationInfo creationInfo = new("Service", 1, 0, 0x8000000, 1, Flags, 0, 0); uint[] defaultCapabilities = { - 0x030363F7, + (((uint)KScheduler.CpuCoresCount - 1) << 24) + (((uint)KScheduler.CpuCoresCount - 1) << 16) + 0x63F7u, 0x1FFFFFCF, 0x207FFFEF, 0x47E0060F, diff --git a/src/Ryujinx.HLE/HOS/Kernel/KernelContext.cs b/src/Ryujinx.HLE/HOS/Kernel/KernelContext.cs index 89d788c54..7e2e9cacc 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/KernelContext.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/KernelContext.cs @@ -63,6 +63,7 @@ namespace Ryujinx.HLE.HOS.Kernel TickSource = tickSource; Device = device; Memory = memory; + KScheduler.CpuCoresCount = device.CpuCoresCount; Running = true; diff --git a/src/Ryujinx.HLE/HOS/Kernel/KernelStatic.cs b/src/Ryujinx.HLE/HOS/Kernel/KernelStatic.cs index f5ecba752..e05fc8397 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/KernelStatic.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/KernelStatic.cs @@ -37,7 +37,7 @@ namespace Ryujinx.HLE.HOS.Kernel return result; } - process.DefaultCpuCore = 3; + process.DefaultCpuCore = KScheduler.CpuCoresCount - 1; context.Processes.TryAdd(process.Pid, process); diff --git a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs index b4aa5ca5c..82c3d2e70 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs @@ -277,7 +277,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process return result; } - result = Capabilities.InitializeForUser(capabilities, MemoryManager); + result = Capabilities.InitializeForUser(capabilities, MemoryManager, IsApplication); if (result != Result.Success) { diff --git a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcessCapabilities.cs b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcessCapabilities.cs index ebab67bb8..5c9f4f100 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcessCapabilities.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcessCapabilities.cs @@ -35,15 +35,15 @@ namespace Ryujinx.HLE.HOS.Kernel.Process DebuggingFlags &= ~3u; KernelReleaseVersion = KProcess.KernelVersionPacked; - return Parse(capabilities, memoryManager); + return Parse(capabilities, memoryManager, false); } - public Result InitializeForUser(ReadOnlySpan capabilities, KPageTableBase memoryManager) + public Result InitializeForUser(ReadOnlySpan capabilities, KPageTableBase memoryManager, bool isApplication) { - return Parse(capabilities, memoryManager); + return Parse(capabilities, memoryManager, isApplication); } - private Result Parse(ReadOnlySpan capabilities, KPageTableBase memoryManager) + private Result Parse(ReadOnlySpan capabilities, KPageTableBase memoryManager, bool isApplication) { int mask0 = 0; int mask1 = 0; @@ -54,7 +54,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process if (cap.GetCapabilityType() != CapabilityType.MapRange) { - Result result = ParseCapability(cap, ref mask0, ref mask1, memoryManager); + Result result = ParseCapability(cap, ref mask0, ref mask1, memoryManager, isApplication); if (result != Result.Success) { @@ -120,7 +120,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process return Result.Success; } - private Result ParseCapability(uint cap, ref int mask0, ref int mask1, KPageTableBase memoryManager) + private Result ParseCapability(uint cap, ref int mask0, ref int mask1, KPageTableBase memoryManager, bool isApplication) { CapabilityType code = cap.GetCapabilityType(); @@ -176,6 +176,11 @@ namespace Ryujinx.HLE.HOS.Kernel.Process AllowedCpuCoresMask = GetMaskFromMinMax(lowestCpuCore, highestCpuCore); AllowedThreadPriosMask = GetMaskFromMinMax(lowestThreadPrio, highestThreadPrio); + if (isApplication && lowestCpuCore == 0 && highestCpuCore != 2) + Ryujinx.Common.Logging.Logger.Error?.Print(Ryujinx.Common.Logging.LogClass.Application, $"Application requested cores with index range {lowestCpuCore} to {highestCpuCore}! Report this to @LotP on the Ryujinx/Ryubing discord server (discord.gg/ryujinx)!"); + else if (isApplication) + Ryujinx.Common.Logging.Logger.Info?.Print(Ryujinx.Common.Logging.LogClass.Application, $"Application requested cores with index range {lowestCpuCore} to {highestCpuCore}"); + break; } diff --git a/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs b/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs index 2f487243d..1b6433af6 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs @@ -2683,7 +2683,7 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return KernelResult.InvalidCombination; } - if ((uint)preferredCore > 3) + if ((uint)preferredCore > KScheduler.CpuCoresCount - 1) { if ((preferredCore | 2) != -1) { diff --git a/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs b/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs index 8ef77902c..19f1b8be0 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs @@ -9,13 +9,11 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading partial class KScheduler : IDisposable { public const int PrioritiesCount = 64; - public const int CpuCoresCount = 4; + public static int CpuCoresCount; private const int RoundRobinTimeQuantumMs = 10; - private static readonly int[] _preemptionPriorities = { 59, 59, 59, 63 }; - - private static readonly int[] _srcCoresHighestPrioThreads = new int[CpuCoresCount]; + private static int[] _srcCoresHighestPrioThreads; private readonly KernelContext _context; private readonly int _coreId; @@ -47,6 +45,16 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading _coreId = coreId; _currentThread = null; + + if (_srcCoresHighestPrioThreads == null) + { + _srcCoresHighestPrioThreads = new int[CpuCoresCount]; + } + } + + private static int PreemptionPriorities(int index) + { + return index == CpuCoresCount - 1 ? 63 : 59; } public static ulong SelectThreads(KernelContext context) @@ -437,7 +445,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading for (int core = 0; core < CpuCoresCount; core++) { - RotateScheduledQueue(context, core, _preemptionPriorities[core]); + RotateScheduledQueue(context, core, PreemptionPriorities(core)); } context.CriticalSection.Leave(); diff --git a/src/Ryujinx.HLE/HOS/Services/ServerBase.cs b/src/Ryujinx.HLE/HOS/Services/ServerBase.cs index f67699b90..40329aa36 100644 --- a/src/Ryujinx.HLE/HOS/Services/ServerBase.cs +++ b/src/Ryujinx.HLE/HOS/Services/ServerBase.cs @@ -24,14 +24,14 @@ namespace Ryujinx.HLE.HOS.Services // not large enough. private const int PointerBufferSize = 0x8000; - private readonly static uint[] _defaultCapabilities = { - 0x030363F7, + private static uint[] _defaultCapabilities => [ + (((uint)KScheduler.CpuCoresCount - 1) << 24) + (((uint)KScheduler.CpuCoresCount - 1) << 16) + 0x63F7u, 0x1FFFFFCF, 0x207FFFEF, 0x47E0060F, 0x0048BFFF, 0x01007FFF, - }; + ]; // The amount of time Dispose() will wait to Join() the thread executing the ServerLoop() private static readonly TimeSpan _threadJoinTimeout = TimeSpan.FromSeconds(3); diff --git a/src/Ryujinx.HLE/Switch.cs b/src/Ryujinx.HLE/Switch.cs index 25e65354f..e15fab03a 100644 --- a/src/Ryujinx.HLE/Switch.cs +++ b/src/Ryujinx.HLE/Switch.cs @@ -32,6 +32,8 @@ namespace Ryujinx.HLE public TamperMachine TamperMachine { get; } public IHostUIHandler UIHandler { get; } + public int CpuCoresCount = 4; //Switch 1 has 4 cores + public VSyncMode VSyncMode { get; set; } = VSyncMode.Switch; public bool CustomVSyncIntervalEnabled { get; set; } = false; public int CustomVSyncInterval { get; set; } diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index 7c4978f0f..7d619b78a 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -22722,6 +22722,31 @@ "zh_TW": "" } }, + { + "ID": "CompatibilityListLastUpdated", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Last updated: {0}", + "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": "CompatibilityListWarning", "Translations": { diff --git a/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs b/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs index 19d2fb94e..7d75ac7c1 100644 --- a/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs +++ b/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs @@ -1,4 +1,4 @@ -using DiscordRPC; +using DiscordRPC; using LibHac.Tools.FsSystem; using Ryujinx.Audio.Backends.SDL2; using Ryujinx.Ava; diff --git a/src/Ryujinx/UI/Helpers/PlayabilityStatusConverter.cs b/src/Ryujinx/UI/Helpers/PlayabilityStatusConverter.cs index a894f0246..99c6a0fce 100644 --- a/src/Ryujinx/UI/Helpers/PlayabilityStatusConverter.cs +++ b/src/Ryujinx/UI/Helpers/PlayabilityStatusConverter.cs @@ -12,8 +12,8 @@ namespace Ryujinx.Ava.UI.Helpers private static readonly Lazy _shared = new(() => new()); public static PlayabilityStatusConverter Shared => _shared.Value; - public object Convert(object? value, Type _, object? __, CultureInfo ___) => - value.Cast() switch + public object Convert(object value, Type _, object __, CultureInfo ___) + => value.Cast() switch { LocaleKeys.CompatibilityListNothing or LocaleKeys.CompatibilityListBoots or @@ -22,7 +22,7 @@ namespace Ryujinx.Ava.UI.Helpers _ => Brushes.ForestGreen }; - public object ConvertBack(object? value, Type _, object? __, CultureInfo ___) + public object ConvertBack(object value, Type _, object __, CultureInfo ___) => throw new NotSupportedException(); } } diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 25e1136b8..5ae9ef9ef 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -741,7 +741,10 @@ namespace Ryujinx.Ava.UI.ViewModels Applications.ToObservableChangeSet() .Filter(Filter) .Sort(GetComparer()) - .Bind(out _appsObservableList).AsObservableList(); +#pragma warning disable MVVMTK0034 + .Bind(out _appsObservableList) +#pragma warning restore MVVMTK0034 + .AsObservableList(); OnPropertyChanged(nameof(AppsObservableList)); } diff --git a/src/Ryujinx/Utilities/AppletMetadata.cs b/src/Ryujinx/Utilities/AppletMetadata.cs index 82baed7d3..42c23ee12 100644 --- a/src/Ryujinx/Utilities/AppletMetadata.cs +++ b/src/Ryujinx/Utilities/AppletMetadata.cs @@ -32,29 +32,27 @@ namespace Ryujinx.Ava.Utilities public string GetContentPath(ContentManager contentManager) => (contentManager ?? _contentManager) - .GetInstalledContentPath(ProgramId, StorageId.BuiltInSystem, NcaContentType.Program); + ?.GetInstalledContentPath(ProgramId, StorageId.BuiltInSystem, NcaContentType.Program); public bool CanStart(ContentManager contentManager, out ApplicationData appData, out BlitStruct appControl) { contentManager ??= _contentManager; - if (contentManager == null) - { - appData = null; - appControl = new BlitStruct(0); - return false; - } + if (contentManager == null) + goto BadData; + + string contentPath = GetContentPath(contentManager); + if (string.IsNullOrEmpty(contentPath)) + goto BadData; appData = new() { Name = Name, Id = ProgramId, Path = GetContentPath(contentManager) }; - - if (string.IsNullOrEmpty(appData.Path)) - { - appControl = new BlitStruct(0); - return false; - } - appControl = StructHelpers.CreateCustomNacpData(Name, Version); return true; + + BadData: + appData = null; + appControl = new BlitStruct(0); + return false; } } } diff --git a/src/Ryujinx/Utilities/Compat/CompatibilityCsv.cs b/src/Ryujinx/Utilities/Compat/CompatibilityCsv.cs index 1e69b42d5..676cf1a52 100644 --- a/src/Ryujinx/Utilities/Compat/CompatibilityCsv.cs +++ b/src/Ryujinx/Utilities/Compat/CompatibilityCsv.cs @@ -1,50 +1,66 @@ using Gommon; +using Humanizer; using nietras.SeparatedValues; using Ryujinx.Ava.Common.Locale; +using Ryujinx.Common.Logging; using System; -using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; using System.Text; namespace Ryujinx.Ava.Utilities.Compat { + public struct ColumnIndices(Func, int> getIndex) + { + public const string TitleIdCol = "\"title_id\""; + public const string GameNameCol = "\"game_name\""; + public const string LabelsCol = "\"labels\""; + public const string StatusCol = "\"status\""; + public const string LastUpdatedCol = "\"last_updated\""; + + public readonly int TitleId = getIndex(TitleIdCol); + public readonly int GameName = getIndex(GameNameCol); + public readonly int Labels = getIndex(LabelsCol); + public readonly int Status = getIndex(StatusCol); + public readonly int LastUpdated = getIndex(LastUpdatedCol); + } + public class CompatibilityCsv { - public static CompatibilityCsv Shared { get; set; } - - public CompatibilityCsv(SepReader reader) + static CompatibilityCsv() { - var entries = new List(); + using Stream csvStream = Assembly.GetExecutingAssembly() + .GetManifestResourceStream("RyujinxGameCompatibilityList")!; + csvStream.Position = 0; - foreach (var row in reader) - { - entries.Add(new CompatibilityEntry(reader.Header, row)); - } + using SepReader reader = Sep.Reader().From(csvStream); + ColumnIndices columnIndices = new(reader.Header.IndexOf); - Entries = entries.Where(x => x.Status != null) - .OrderBy(it => it.GameName).ToArray(); + Entries = reader + .Enumerate(row => new CompatibilityEntry(ref columnIndices, row)) + .OrderBy(it => it.GameName) + .ToArray(); + + Logger.Debug?.Print(LogClass.UI, "Compatibility CSV loaded.", "LoadCompatCsv"); } - public CompatibilityEntry[] Entries { get; } + public static CompatibilityEntry[] Entries { get; private set; } } public class CompatibilityEntry { - public CompatibilityEntry(SepReaderHeader header, SepReader.Row row) + public CompatibilityEntry(ref ColumnIndices indices, SepReader.Row row) { - if (row.ColCount != header.ColNames.Count) - throw new InvalidDataException($"CSV row {row.RowIndex} ({row.ToString()}) has mismatched column count"); - - var titleIdRow = ColStr(row[header.IndexOf("\"title_id\"")]); + string titleIdRow = ColStr(row[indices.TitleId]); TitleId = !string.IsNullOrEmpty(titleIdRow) ? titleIdRow : default(Optional); - GameName = ColStr(row[header.IndexOf("\"game_name\"")]).Trim().Trim('"'); + GameName = ColStr(row[indices.GameName]).Trim().Trim('"'); - IssueLabels = ColStr(row[header.IndexOf("\"labels\"")]).Split(';'); - Status = ColStr(row[header.IndexOf("\"status\"")]).ToLower() switch + Labels = ColStr(row[indices.Labels]).Split(';'); + Status = ColStr(row[indices.Status]).ToLower() switch { "playable" => LocaleKeys.CompatibilityListPlayable, "ingame" => LocaleKeys.CompatibilityListIngame, @@ -54,8 +70,8 @@ namespace Ryujinx.Ava.Utilities.Compat _ => null }; - if (DateTime.TryParse(ColStr(row[header.IndexOf("\"last_updated\"")]), out var dt)) - LastEvent = dt; + if (DateTime.TryParse(ColStr(row[indices.LastUpdated]), out var dt)) + LastUpdated = dt; return; @@ -64,27 +80,32 @@ namespace Ryujinx.Ava.Utilities.Compat public string GameName { get; } public Optional TitleId { get; } - public string[] IssueLabels { get; } + public string[] Labels { get; } public LocaleKeys? Status { get; } - public DateTime LastEvent { get; } + public DateTime LastUpdated { get; } + + public string LocalizedLastUpdated => + LocaleManager.FormatDynamicValue(LocaleKeys.CompatibilityListLastUpdated, LastUpdated.Humanize()); public string LocalizedStatus => LocaleManager.Instance[Status!.Value]; public string FormattedTitleId => TitleId .OrElse(new string(' ', 16)); - public string FormattedIssueLabels => IssueLabels + public string FormattedIssueLabels => Labels .Where(it => !it.StartsWithIgnoreCase("status")) .Select(FormatLabelName) .JoinToString(", "); public override string ToString() { - var sb = new StringBuilder("CompatibilityEntry: {"); + StringBuilder sb = new("CompatibilityEntry: {"); sb.Append($"{nameof(GameName)}=\"{GameName}\", "); sb.Append($"{nameof(TitleId)}={TitleId}, "); - sb.Append($"{nameof(IssueLabels)}=\"{IssueLabels}\", "); + sb.Append($"{nameof(Labels)}={ + Labels.FormatCollection(it => $"\"{it}\"", separator: ", ", prefix: "[", suffix: "]") + }, "); sb.Append($"{nameof(Status)}=\"{Status}\", "); - sb.Append($"{nameof(LastEvent)}=\"{LastEvent}\""); + sb.Append($"{nameof(LastUpdated)}=\"{LastUpdated}\""); sb.Append('}'); return sb.ToString(); @@ -140,8 +161,8 @@ namespace Ryujinx.Ava.Utilities.Compat if (value == string.Empty) return string.Empty; - var firstChar = value[0]; - var rest = value[1..]; + char firstChar = value[0]; + string rest = value[1..]; return $"{char.ToUpper(firstChar)}{rest}"; } diff --git a/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml b/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml index 8e14c3904..73ec84c53 100644 --- a/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml +++ b/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml @@ -44,8 +44,11 @@ ItemsSource="{Binding CurrentEntries}"> - + _currentEntries = CompatibilityCsv.Shared.Entries; + private IEnumerable _currentEntries = CompatibilityCsv.Entries; private readonly string[] _ownedGameTitleIds = []; private readonly ApplicationLibrary _appLibrary; public IEnumerable CurrentEntries => OnlyShowOwnedGames ? _currentEntries.Where(x => - x.TitleId.Check(tid => _ownedGameTitleIds.ContainsIgnoreCase(tid)) - || _appLibrary.Applications.Items.Any(a => a.Name.EqualsIgnoreCase(x.GameName))) + x.TitleId.Check(tid => _ownedGameTitleIds.ContainsIgnoreCase(tid))) : _currentEntries; public CompatibilityViewModel() {} @@ -39,11 +38,11 @@ namespace Ryujinx.Ava.Utilities.Compat { if (string.IsNullOrEmpty(searchTerm)) { - SetEntries(CompatibilityCsv.Shared.Entries); + SetEntries(CompatibilityCsv.Entries); return; } - SetEntries(CompatibilityCsv.Shared.Entries.Where(x => + SetEntries(CompatibilityCsv.Entries.Where(x => x.GameName.ContainsIgnoreCase(searchTerm) || x.TitleId.Check(tid => tid.ContainsIgnoreCase(searchTerm)))); }