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))));
}