aboutsummaryrefslogtreecommitdiff
path: root/src/Ryujinx.Common
diff options
context:
space:
mode:
authorTSR Berry <20988865+TSRBerry@users.noreply.github.com>2023-04-08 01:22:00 +0200
committerMary <thog@protonmail.com>2023-04-27 23:51:14 +0200
commitcee712105850ac3385cd0091a923438167433f9f (patch)
tree4a5274b21d8b7f938c0d0ce18736d3f2993b11b1 /src/Ryujinx.Common
parentcd124bda587ef09668a971fa1cac1c3f0cfc9f21 (diff)
Move solution and projects to src
Diffstat (limited to 'src/Ryujinx.Common')
-rw-r--r--src/Ryujinx.Common/AsyncWorkQueue.cs100
-rw-r--r--src/Ryujinx.Common/Collections/IntervalTree.cs499
-rw-r--r--src/Ryujinx.Common/Collections/IntrusiveRedBlackTree.cs285
-rw-r--r--src/Ryujinx.Common/Collections/IntrusiveRedBlackTreeImpl.cs354
-rw-r--r--src/Ryujinx.Common/Collections/IntrusiveRedBlackTreeNode.cs16
-rw-r--r--src/Ryujinx.Common/Collections/TreeDictionary.cs617
-rw-r--r--src/Ryujinx.Common/Configuration/AntiAliasing.cs16
-rw-r--r--src/Ryujinx.Common/Configuration/AppDataManager.cs149
-rw-r--r--src/Ryujinx.Common/Configuration/AspectRatioExtensions.cs63
-rw-r--r--src/Ryujinx.Common/Configuration/BackendThreading.cs13
-rw-r--r--src/Ryujinx.Common/Configuration/DownloadableContentContainer.cs13
-rw-r--r--src/Ryujinx.Common/Configuration/DownloadableContentJsonSerializerContext.cs11
-rw-r--r--src/Ryujinx.Common/Configuration/DownloadableContentNca.cs14
-rw-r--r--src/Ryujinx.Common/Configuration/GraphicsBackend.cs12
-rw-r--r--src/Ryujinx.Common/Configuration/GraphicsDebugLevel.cs14
-rw-r--r--src/Ryujinx.Common/Configuration/Hid/Controller/GamepadInputId.cs58
-rw-r--r--src/Ryujinx.Common/Configuration/Hid/Controller/GenericControllerInputConfig.cs82
-rw-r--r--src/Ryujinx.Common/Configuration/Hid/Controller/JoyconConfigControllerStick.cs11
-rw-r--r--src/Ryujinx.Common/Configuration/Hid/Controller/Motion/CemuHookMotionConfigController.cs30
-rw-r--r--src/Ryujinx.Common/Configuration/Hid/Controller/Motion/JsonMotionConfigControllerConverter.cs79
-rw-r--r--src/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionConfigController.cs25
-rw-r--r--src/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionConfigJsonSerializerContext.cs12
-rw-r--r--src/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionInputBackendType.cs13
-rw-r--r--src/Ryujinx.Common/Configuration/Hid/Controller/Motion/StandardMotionConfigController.cs4
-rw-r--r--src/Ryujinx.Common/Configuration/Hid/Controller/RumbleConfigController.cs20
-rw-r--r--src/Ryujinx.Common/Configuration/Hid/Controller/StandardControllerInputConfig.cs4
-rw-r--r--src/Ryujinx.Common/Configuration/Hid/Controller/StickInputId.cs15
-rw-r--r--src/Ryujinx.Common/Configuration/Hid/ControllerType.cs23
-rw-r--r--src/Ryujinx.Common/Configuration/Hid/GenericInputConfigurationCommon.cs15
-rw-r--r--src/Ryujinx.Common/Configuration/Hid/InputBackendType.cs13
-rw-r--r--src/Ryujinx.Common/Configuration/Hid/InputConfig.cs41
-rw-r--r--src/Ryujinx.Common/Configuration/Hid/InputConfigJsonSerializerContext.cs14
-rw-r--r--src/Ryujinx.Common/Configuration/Hid/JsonInputConfigConverter.cs81
-rw-r--r--src/Ryujinx.Common/Configuration/Hid/Key.cs143
-rw-r--r--src/Ryujinx.Common/Configuration/Hid/Keyboard/GenericKeyboardInputConfig.cs15
-rw-r--r--src/Ryujinx.Common/Configuration/Hid/Keyboard/JoyconConfigKeyboardStick.cs11
-rw-r--r--src/Ryujinx.Common/Configuration/Hid/Keyboard/StandardKeyboardInputConfig.cs4
-rw-r--r--src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs17
-rw-r--r--src/Ryujinx.Common/Configuration/Hid/LeftJoyconCommonConfig.cs15
-rw-r--r--src/Ryujinx.Common/Configuration/Hid/PlayerIndex.cs22
-rw-r--r--src/Ryujinx.Common/Configuration/Hid/RightJoyconCommonConfig.cs15
-rw-r--r--src/Ryujinx.Common/Configuration/MemoryManagerMode.cs13
-rw-r--r--src/Ryujinx.Common/Configuration/ScalingFilter.cs13
-rw-r--r--src/Ryujinx.Common/Configuration/TitleUpdateMetadata.cs10
-rw-r--r--src/Ryujinx.Common/Configuration/TitleUpdateMetadataJsonSerializerContext.cs10
-rw-r--r--src/Ryujinx.Common/Extensions/BinaryReaderExtensions.cs16
-rw-r--r--src/Ryujinx.Common/Extensions/BinaryWriterExtensions.cs28
-rw-r--r--src/Ryujinx.Common/Extensions/StreamExtensions.cs138
-rw-r--r--src/Ryujinx.Common/GraphicsDriver/DriverUtilities.cs22
-rw-r--r--src/Ryujinx.Common/GraphicsDriver/NVAPI/Nvapi.cs11
-rw-r--r--src/Ryujinx.Common/GraphicsDriver/NVAPI/NvapiUnicodeString.cs42
-rw-r--r--src/Ryujinx.Common/GraphicsDriver/NVAPI/NvdrsApplicationV4.cs17
-rw-r--r--src/Ryujinx.Common/GraphicsDriver/NVAPI/NvdrsProfile.cs15
-rw-r--r--src/Ryujinx.Common/GraphicsDriver/NVAPI/NvdrsSetting.cs49
-rw-r--r--src/Ryujinx.Common/GraphicsDriver/NVThreadedOptimization.cs163
-rw-r--r--src/Ryujinx.Common/Hash128.cs48
-rw-r--r--src/Ryujinx.Common/Logging/Formatters/DefaultLogFormatter.cs42
-rw-r--r--src/Ryujinx.Common/Logging/Formatters/DynamicObjectFormatter.cs84
-rw-r--r--src/Ryujinx.Common/Logging/Formatters/ILogFormatter.cs7
-rw-r--r--src/Ryujinx.Common/Logging/LogClass.cs76
-rw-r--r--src/Ryujinx.Common/Logging/LogEventArgs.cs23
-rw-r--r--src/Ryujinx.Common/Logging/LogEventArgsJson.cs30
-rw-r--r--src/Ryujinx.Common/Logging/LogEventJsonSerializerContext.cs9
-rw-r--r--src/Ryujinx.Common/Logging/LogLevel.cs19
-rw-r--r--src/Ryujinx.Common/Logging/Logger.cs224
-rw-r--r--src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs79
-rw-r--r--src/Ryujinx.Common/Logging/Targets/ConsoleLogTarget.cs41
-rw-r--r--src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs55
-rw-r--r--src/Ryujinx.Common/Logging/Targets/ILogTarget.cs11
-rw-r--r--src/Ryujinx.Common/Logging/Targets/JsonLogTarget.cs40
-rw-r--r--src/Ryujinx.Common/Memory/ArrayPtr.cs123
-rw-r--r--src/Ryujinx.Common/Memory/Box.cs12
-rw-r--r--src/Ryujinx.Common/Memory/ByteMemoryPool.ByteMemoryPoolBuffer.cs51
-rw-r--r--src/Ryujinx.Common/Memory/ByteMemoryPool.cs108
-rw-r--r--src/Ryujinx.Common/Memory/IArray.cs21
-rw-r--r--src/Ryujinx.Common/Memory/MemoryStreamManager.cs99
-rw-r--r--src/Ryujinx.Common/Memory/PartialUnmaps/NativeReaderWriterLock.cs80
-rw-r--r--src/Ryujinx.Common/Memory/PartialUnmaps/PartialUnmapHelpers.cs20
-rw-r--r--src/Ryujinx.Common/Memory/PartialUnmaps/PartialUnmapState.cs163
-rw-r--r--src/Ryujinx.Common/Memory/PartialUnmaps/ThreadLocalMap.cs92
-rw-r--r--src/Ryujinx.Common/Memory/Ptr.cs68
-rw-r--r--src/Ryujinx.Common/Memory/SpanOrArray.cs89
-rw-r--r--src/Ryujinx.Common/Memory/SpanReader.cs56
-rw-r--r--src/Ryujinx.Common/Memory/SpanWriter.cs45
-rw-r--r--src/Ryujinx.Common/Memory/StructArrayHelpers.cs654
-rw-r--r--src/Ryujinx.Common/Memory/StructByteArrayHelpers.cs77
-rw-r--r--src/Ryujinx.Common/PerformanceCounter.cs82
-rw-r--r--src/Ryujinx.Common/Pools/ObjectPool.cs75
-rw-r--r--src/Ryujinx.Common/Pools/SharedPools.cs17
-rw-r--r--src/Ryujinx.Common/Pools/ThreadStaticArray.cs20
-rw-r--r--src/Ryujinx.Common/ReactiveObject.cs61
-rw-r--r--src/Ryujinx.Common/ReferenceEqualityComparer.cs19
-rw-r--r--src/Ryujinx.Common/ReleaseInformation.cs60
-rw-r--r--src/Ryujinx.Common/Ryujinx.Common.csproj15
-rw-r--r--src/Ryujinx.Common/SystemInfo/LinuxSystemInfo.cs80
-rw-r--r--src/Ryujinx.Common/SystemInfo/MacOSSystemInfo.cs157
-rw-r--r--src/Ryujinx.Common/SystemInfo/SystemInfo.cs80
-rw-r--r--src/Ryujinx.Common/SystemInfo/WindowsSystemInfo.cs89
-rw-r--r--src/Ryujinx.Common/SystemInterop/DisplaySleep.cs35
-rw-r--r--src/Ryujinx.Common/SystemInterop/ForceDpiAware.cs96
-rw-r--r--src/Ryujinx.Common/SystemInterop/GdiPlusHelper.cs76
-rw-r--r--src/Ryujinx.Common/SystemInterop/StdErrAdapter.cs109
-rw-r--r--src/Ryujinx.Common/SystemInterop/WindowsMultimediaTimerResolution.cs114
-rw-r--r--src/Ryujinx.Common/Utilities/BitUtils.cs60
-rw-r--r--src/Ryujinx.Common/Utilities/BitfieldExtensions.cs57
-rw-r--r--src/Ryujinx.Common/Utilities/Buffers.cs59
-rw-r--r--src/Ryujinx.Common/Utilities/CommonJsonContext.cs11
-rw-r--r--src/Ryujinx.Common/Utilities/EmbeddedResources.cs148
-rw-r--r--src/Ryujinx.Common/Utilities/HexUtils.cs90
-rw-r--r--src/Ryujinx.Common/Utilities/JsonHelper.cs98
-rw-r--r--src/Ryujinx.Common/Utilities/MessagePackObjectFormatter.cs302
-rw-r--r--src/Ryujinx.Common/Utilities/NetworkHelpers.cs66
-rw-r--r--src/Ryujinx.Common/Utilities/SpanHelpers.cs61
-rw-r--r--src/Ryujinx.Common/Utilities/StreamUtils.cs31
-rw-r--r--src/Ryujinx.Common/Utilities/TypedStringEnumConverter.cs34
-rw-r--r--src/Ryujinx.Common/Utilities/UInt128Utils.cs18
-rw-r--r--src/Ryujinx.Common/XXHash128.cs537
117 files changed, 8808 insertions, 0 deletions
diff --git a/src/Ryujinx.Common/AsyncWorkQueue.cs b/src/Ryujinx.Common/AsyncWorkQueue.cs
new file mode 100644
index 00000000..80f8dcfe
--- /dev/null
+++ b/src/Ryujinx.Common/AsyncWorkQueue.cs
@@ -0,0 +1,100 @@
+using System;
+using System.Collections.Concurrent;
+using System.Threading;
+
+namespace Ryujinx.Common
+{
+ public sealed class AsyncWorkQueue<T> : IDisposable
+ {
+ private readonly Thread _workerThread;
+ private readonly CancellationTokenSource _cts;
+ private readonly Action<T> _workerAction;
+ private readonly BlockingCollection<T> _queue;
+
+ public bool IsCancellationRequested => _cts.IsCancellationRequested;
+
+ public AsyncWorkQueue(Action<T> callback, string name = null) : this(callback, name, new BlockingCollection<T>())
+ {
+ }
+
+ public AsyncWorkQueue(Action<T> callback, string name, BlockingCollection<T> collection)
+ {
+ _cts = new CancellationTokenSource();
+ _queue = collection;
+ _workerAction = callback;
+ _workerThread = new Thread(DoWork) { Name = name };
+
+ _workerThread.IsBackground = true;
+ _workerThread.Start();
+ }
+
+ private void DoWork()
+ {
+ try
+ {
+ foreach (var item in _queue.GetConsumingEnumerable(_cts.Token))
+ {
+ _workerAction(item);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ }
+ }
+
+ public void Cancel()
+ {
+ _cts.Cancel();
+ }
+
+ public void CancelAfter(int millisecondsDelay)
+ {
+ _cts.CancelAfter(millisecondsDelay);
+ }
+
+ public void CancelAfter(TimeSpan delay)
+ {
+ _cts.CancelAfter(delay);
+ }
+
+ public void Add(T workItem)
+ {
+ _queue.Add(workItem);
+ }
+
+ public void Add(T workItem, CancellationToken cancellationToken)
+ {
+ _queue.Add(workItem, cancellationToken);
+ }
+
+ public bool TryAdd(T workItem)
+ {
+ return _queue.TryAdd(workItem);
+ }
+
+ public bool TryAdd(T workItem, int millisecondsDelay)
+ {
+ return _queue.TryAdd(workItem, millisecondsDelay);
+ }
+
+ public bool TryAdd(T workItem, int millisecondsDelay, CancellationToken cancellationToken)
+ {
+ return _queue.TryAdd(workItem, millisecondsDelay, cancellationToken);
+ }
+
+ public bool TryAdd(T workItem, TimeSpan timeout)
+ {
+ return _queue.TryAdd(workItem, timeout);
+ }
+
+ public void Dispose()
+ {
+ _queue.CompleteAdding();
+ _cts.Cancel();
+ _workerThread.Join();
+
+ _queue.Dispose();
+ _cts.Dispose();
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Collections/IntervalTree.cs b/src/Ryujinx.Common/Collections/IntervalTree.cs
new file mode 100644
index 00000000..b5188cc7
--- /dev/null
+++ b/src/Ryujinx.Common/Collections/IntervalTree.cs
@@ -0,0 +1,499 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Ryujinx.Common.Collections
+{
+ /// <summary>
+ /// An Augmented Interval Tree based off of the "TreeDictionary"'s Red-Black Tree. Allows fast overlap checking of ranges.
+ /// </summary>
+ /// <typeparam name="K">Key</typeparam>
+ /// <typeparam name="V">Value</typeparam>
+ public class IntervalTree<K, V> : IntrusiveRedBlackTreeImpl<IntervalTreeNode<K, V>> where K : IComparable<K>
+ {
+ private const int ArrayGrowthSize = 32;
+
+ #region Public Methods
+
+ /// <summary>
+ /// Gets the values of the interval whose key is <paramref name="key"/>.
+ /// </summary>
+ /// <param name="key">Key of the node value to get</param>
+ /// <param name="overlaps">Overlaps array to place results in</param>
+ /// <returns>Number of values found</returns>
+ /// <exception cref="ArgumentNullException"><paramref name="key"/> is null</exception>
+ public int Get(K key, ref V[] overlaps)
+ {
+ ArgumentNullException.ThrowIfNull(key);
+
+ IntervalTreeNode<K, V> node = GetNode(key);
+
+ if (node == null)
+ {
+ return 0;
+ }
+
+ if (node.Values.Count > overlaps.Length)
+ {
+ Array.Resize(ref overlaps, node.Values.Count);
+ }
+
+ int overlapsCount = 0;
+ foreach (RangeNode<K, V> value in node.Values)
+ {
+ overlaps[overlapsCount++] = value.Value;
+ }
+
+ return overlapsCount;
+ }
+
+ /// <summary>
+ /// Returns the values of the intervals whose start and end keys overlap the given range.
+ /// </summary>
+ /// <param name="start">Start of the range</param>
+ /// <param name="end">End of the range</param>
+ /// <param name="overlaps">Overlaps array to place results in</param>
+ /// <param name="overlapCount">Index to start writing results into the array. Defaults to 0</param>
+ /// <returns>Number of values found</returns>
+ /// <exception cref="ArgumentNullException"><paramref name="start"/> or <paramref name="end"/> is null</exception>
+ public int Get(K start, K end, ref V[] overlaps, int overlapCount = 0)
+ {
+ ArgumentNullException.ThrowIfNull(start);
+ ArgumentNullException.ThrowIfNull(end);
+
+ GetValues(Root, start, end, ref overlaps, ref overlapCount);
+
+ return overlapCount;
+ }
+
+ /// <summary>
+ /// Adds a new interval into the tree whose start is <paramref name="start"/>, end is <paramref name="end"/> and value is <paramref name="value"/>.
+ /// </summary>
+ /// <param name="start">Start of the range to add</param>
+ /// <param name="end">End of the range to insert</param>
+ /// <param name="value">Value to add</param>
+ /// <exception cref="ArgumentNullException"><paramref name="start"/>, <paramref name="end"/> or <paramref name="value"/> are null</exception>
+ public void Add(K start, K end, V value)
+ {
+ ArgumentNullException.ThrowIfNull(start);
+ ArgumentNullException.ThrowIfNull(end);
+ ArgumentNullException.ThrowIfNull(value);
+
+ Insert(start, end, value);
+ }
+
+ /// <summary>
+ /// Removes the given <paramref name="value"/> from the tree, searching for it with <paramref name="key"/>.
+ /// </summary>
+ /// <param name="key">Key of the node to remove</param>
+ /// <param name="value">Value to remove</param>
+ /// <exception cref="ArgumentNullException"><paramref name="key"/> is null</exception>
+ /// <returns>Number of deleted values</returns>
+ public int Remove(K key, V value)
+ {
+ ArgumentNullException.ThrowIfNull(key);
+
+ int removed = Delete(key, value);
+
+ Count -= removed;
+
+ return removed;
+ }
+
+ /// <summary>
+ /// Adds all the nodes in the dictionary into <paramref name="list"/>.
+ /// </summary>
+ /// <returns>A list of all RangeNodes sorted by Key Order</returns>
+ public List<RangeNode<K, V>> AsList()
+ {
+ List<RangeNode<K, V>> list = new List<RangeNode<K, V>>();
+
+ AddToList(Root, list);
+
+ return list;
+ }
+
+ #endregion
+
+ #region Private Methods (BST)
+
+ /// <summary>
+ /// Adds all RangeNodes that are children of or contained within <paramref name="node"/> into <paramref name="list"/>, in Key Order.
+ /// </summary>
+ /// <param name="node">The node to search for RangeNodes within</param>
+ /// <param name="list">The list to add RangeNodes to</param>
+ private void AddToList(IntervalTreeNode<K, V> node, List<RangeNode<K, V>> list)
+ {
+ if (node == null)
+ {
+ return;
+ }
+
+ AddToList(node.Left, list);
+
+ list.AddRange(node.Values);
+
+ AddToList(node.Right, list);
+ }
+
+ /// <summary>
+ /// Retrieve the node reference whose key is <paramref name="key"/>, or null if no such node exists.
+ /// </summary>
+ /// <param name="key">Key of the node to get</param>
+ /// <returns>Node reference in the tree</returns>
+ /// <exception cref="ArgumentNullException"><paramref name="key"/> is null</exception>
+ private IntervalTreeNode<K, V> GetNode(K key)
+ {
+ ArgumentNullException.ThrowIfNull(key);
+
+ IntervalTreeNode<K, V> node = Root;
+ while (node != null)
+ {
+ int cmp = key.CompareTo(node.Start);
+ if (cmp < 0)
+ {
+ node = node.Left;
+ }
+ else if (cmp > 0)
+ {
+ node = node.Right;
+ }
+ else
+ {
+ return node;
+ }
+ }
+ return null;
+ }
+
+ /// <summary>
+ /// Retrieve all values that overlap the given start and end keys.
+ /// </summary>
+ /// <param name="start">Start of the range</param>
+ /// <param name="end">End of the range</param>
+ /// <param name="overlaps">Overlaps array to place results in</param>
+ /// <param name="overlapCount">Overlaps count to update</param>
+ private void GetValues(IntervalTreeNode<K, V> node, K start, K end, ref V[] overlaps, ref int overlapCount)
+ {
+ if (node == null || start.CompareTo(node.Max) >= 0)
+ {
+ return;
+ }
+
+ GetValues(node.Left, start, end, ref overlaps, ref overlapCount);
+
+ bool endsOnRight = end.CompareTo(node.Start) > 0;
+ if (endsOnRight)
+ {
+ if (start.CompareTo(node.End) < 0)
+ {
+ // Contains this node. Add overlaps to list.
+ foreach (RangeNode<K,V> overlap in node.Values)
+ {
+ if (start.CompareTo(overlap.End) < 0)
+ {
+ if (overlaps.Length >= overlapCount)
+ {
+ Array.Resize(ref overlaps, overlapCount + ArrayGrowthSize);
+ }
+
+ overlaps[overlapCount++] = overlap.Value;
+ }
+ }
+ }
+
+ GetValues(node.Right, start, end, ref overlaps, ref overlapCount);
+ }
+ }
+
+ /// <summary>
+ /// Inserts a new node into the tree with a given <paramref name="start"/>, <paramref name="end"/> and <paramref name="value"/>.
+ /// </summary>
+ /// <param name="start">Start of the range to insert</param>
+ /// <param name="end">End of the range to insert</param>
+ /// <param name="value">Value to insert</param>
+ private void Insert(K start, K end, V value)
+ {
+ IntervalTreeNode<K, V> newNode = BSTInsert(start, end, value);
+ RestoreBalanceAfterInsertion(newNode);
+ }
+
+ /// <summary>
+ /// Propagate an increase in max value starting at the given node, heading up the tree.
+ /// This should only be called if the max increases - not for rebalancing or removals.
+ /// </summary>
+ /// <param name="node">The node to start propagating from</param>
+ private void PropagateIncrease(IntervalTreeNode<K, V> node)
+ {
+ K max = node.Max;
+ IntervalTreeNode<K, V> ptr = node;
+
+ while ((ptr = ptr.Parent) != null)
+ {
+ if (max.CompareTo(ptr.Max) > 0)
+ {
+ ptr.Max = max;
+ }
+ else
+ {
+ break;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Propagate recalculating max value starting at the given node, heading up the tree.
+ /// This fully recalculates the max value from all children when there is potential for it to decrease.
+ /// </summary>
+ /// <param name="node">The node to start propagating from</param>
+ private void PropagateFull(IntervalTreeNode<K, V> node)
+ {
+ IntervalTreeNode<K, V> ptr = node;
+
+ do
+ {
+ K max = ptr.End;
+
+ if (ptr.Left != null && ptr.Left.Max.CompareTo(max) > 0)
+ {
+ max = ptr.Left.Max;
+ }
+
+ if (ptr.Right != null && ptr.Right.Max.CompareTo(max) > 0)
+ {
+ max = ptr.Right.Max;
+ }
+
+ ptr.Max = max;
+ } while ((ptr = ptr.Parent) != null);
+ }
+
+ /// <summary>
+ /// Insertion Mechanism for the interval tree. Similar to a BST insert, with the start of the range as the key.
+ /// Iterates the tree starting from the root and inserts a new node where all children in the left subtree are less than <paramref name="start"/>, and all children in the right subtree are greater than <paramref name="start"/>.
+ /// Each node can contain multiple values, and has an end address which is the maximum of all those values.
+ /// Post insertion, the "max" value of the node and all parents are updated.
+ /// </summary>
+ /// <param name="start">Start of the range to insert</param>
+ /// <param name="end">End of the range to insert</param>
+ /// <param name="value">Value to insert</param>
+ /// <returns>The inserted Node</returns>
+ private IntervalTreeNode<K, V> BSTInsert(K start, K end, V value)
+ {
+ IntervalTreeNode<K, V> parent = null;
+ IntervalTreeNode<K, V> node = Root;
+
+ while (node != null)
+ {
+ parent = node;
+ int cmp = start.CompareTo(node.Start);
+ if (cmp < 0)
+ {
+ node = node.Left;
+ }
+ else if (cmp > 0)
+ {
+ node = node.Right;
+ }
+ else
+ {
+ node.Values.Add(new RangeNode<K, V>(start, end, value));
+
+ if (end.CompareTo(node.End) > 0)
+ {
+ node.End = end;
+ if (end.CompareTo(node.Max) > 0)
+ {
+ node.Max = end;
+ PropagateIncrease(node);
+ }
+ }
+
+ Count++;
+ return node;
+ }
+ }
+ IntervalTreeNode<K, V> newNode = new IntervalTreeNode<K, V>(start, end, value, parent);
+ if (newNode.Parent == null)
+ {
+ Root = newNode;
+ }
+ else if (start.CompareTo(parent.Start) < 0)
+ {
+ parent.Left = newNode;
+ }
+ else
+ {
+ parent.Right = newNode;
+ }
+
+ PropagateIncrease(newNode);
+ Count++;
+ return newNode;
+ }
+
+ /// <summary>
+ /// Removes instances of <paramref name="value"> from the dictionary after searching for it with <paramref name="key">.
+ /// </summary>
+ /// <param name="key">Key to search for</param>
+ /// <param name="value">Value to delete</param>
+ /// <returns>Number of deleted values</returns>
+ private int Delete(K key, V value)
+ {
+ IntervalTreeNode<K, V> nodeToDelete = GetNode(key);
+
+ if (nodeToDelete == null)
+ {
+ return 0;
+ }
+
+ int removed = nodeToDelete.Values.RemoveAll(node => node.Value.Equals(value));
+
+ if (nodeToDelete.Values.Count > 0)
+ {
+ if (removed > 0)
+ {
+ nodeToDelete.End = nodeToDelete.Values.Max(node => node.End);
+
+ // Recalculate max from children and new end.
+ PropagateFull(nodeToDelete);
+ }
+
+ return removed;
+ }
+
+ IntervalTreeNode<K, V> replacementNode;
+
+ if (LeftOf(nodeToDelete) == null || RightOf(nodeToDelete) == null)
+ {
+ replacementNode = nodeToDelete;
+ }
+ else
+ {
+ replacementNode = PredecessorOf(nodeToDelete);
+ }
+
+ IntervalTreeNode<K, V> tmp = LeftOf(replacementNode) ?? RightOf(replacementNode);
+
+ if (tmp != null)
+ {
+ tmp.Parent = ParentOf(replacementNode);
+ }
+
+ if (ParentOf(replacementNode) == null)
+ {
+ Root = tmp;
+ }
+ else if (replacementNode == LeftOf(ParentOf(replacementNode)))
+ {
+ ParentOf(replacementNode).Left = tmp;
+ }
+ else
+ {
+ ParentOf(replacementNode).Right = tmp;
+ }
+
+ if (replacementNode != nodeToDelete)
+ {
+ nodeToDelete.Start = replacementNode.Start;
+ nodeToDelete.Values = replacementNode.Values;
+ nodeToDelete.End = replacementNode.End;
+ nodeToDelete.Max = replacementNode.Max;
+ }
+
+ PropagateFull(replacementNode);
+
+ if (tmp != null && ColorOf(replacementNode) == Black)
+ {
+ RestoreBalanceAfterRemoval(tmp);
+ }
+
+ return removed;
+ }
+
+ #endregion
+
+ protected override void RotateLeft(IntervalTreeNode<K, V> node)
+ {
+ if (node != null)
+ {
+ base.RotateLeft(node);
+
+ PropagateFull(node);
+ }
+ }
+
+ protected override void RotateRight(IntervalTreeNode<K, V> node)
+ {
+ if (node != null)
+ {
+ base.RotateRight(node);
+
+ PropagateFull(node);
+ }
+ }
+
+ public bool ContainsKey(K key)
+ {
+ ArgumentNullException.ThrowIfNull(key);
+
+ return GetNode(key) != null;
+ }
+ }
+
+ /// <summary>
+ /// Represents a value and its start and end keys.
+ /// </summary>
+ /// <typeparam name="K"></typeparam>
+ /// <typeparam name="V"></typeparam>
+ public readonly struct RangeNode<K, V>
+ {
+ public readonly K Start;
+ public readonly K End;
+ public readonly V Value;
+
+ public RangeNode(K start, K end, V value)
+ {
+ Start = start;
+ End = end;
+ Value = value;
+ }
+ }
+
+ /// <summary>
+ /// Represents a node in the IntervalTree which contains start and end keys of type K, and a value of generic type V.
+ /// </summary>
+ /// <typeparam name="K">Key type of the node</typeparam>
+ /// <typeparam name="V">Value type of the node</typeparam>
+ public class IntervalTreeNode<K, V> : IntrusiveRedBlackTreeNode<IntervalTreeNode<K, V>>
+ {
+ /// <summary>
+ /// The start of the range.
+ /// </summary>
+ internal K Start;
+
+ /// <summary>
+ /// The end of the range - maximum of all in the Values list.
+ /// </summary>
+ internal K End;
+
+ /// <summary>
+ /// The maximum end value of this node and all its children.
+ /// </summary>
+ internal K Max;
+
+ /// <summary>
+ /// Values contained on the node that shares a common Start value.
+ /// </summary>
+ internal List<RangeNode<K, V>> Values;
+
+ internal IntervalTreeNode(K start, K end, V value, IntervalTreeNode<K, V> parent)
+ {
+ Start = start;
+ End = end;
+ Max = end;
+ Values = new List<RangeNode<K, V>> { new RangeNode<K, V>(start, end, value) };
+ Parent = parent;
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Collections/IntrusiveRedBlackTree.cs b/src/Ryujinx.Common/Collections/IntrusiveRedBlackTree.cs
new file mode 100644
index 00000000..0063d91e
--- /dev/null
+++ b/src/Ryujinx.Common/Collections/IntrusiveRedBlackTree.cs
@@ -0,0 +1,285 @@
+using System;
+
+namespace Ryujinx.Common.Collections
+{
+ /// <summary>
+ /// Tree that provides the ability for O(logN) lookups for keys that exist in the tree, and O(logN) lookups for keys immediately greater than or less than a specified key.
+ /// </summary>
+ /// <typeparam name="T">Derived node type</typeparam>
+ public class IntrusiveRedBlackTree<T> : IntrusiveRedBlackTreeImpl<T> where T : IntrusiveRedBlackTreeNode<T>, IComparable<T>
+ {
+ #region Public Methods
+
+ /// <summary>
+ /// Adds a new node into the tree.
+ /// </summary>
+ /// <param name="node">Node to be added</param>
+ /// <exception cref="ArgumentNullException"><paramref name="node"/> is null</exception>
+ public void Add(T node)
+ {
+ ArgumentNullException.ThrowIfNull(node);
+
+ Insert(node);
+ }
+
+ /// <summary>
+ /// Removes a node from the tree.
+ /// </summary>
+ /// <param name="node">Note to be removed</param>
+ /// <exception cref="ArgumentNullException"><paramref name="node"/> is null</exception>
+ public void Remove(T node)
+ {
+ ArgumentNullException.ThrowIfNull(node);
+
+ if (Delete(node) != null)
+ {
+ Count--;
+ }
+ }
+
+ /// <summary>
+ /// Retrieve the node that is considered equal to the specified node by the comparator.
+ /// </summary>
+ /// <param name="searchNode">Node to compare with</param>
+ /// <returns>Node that is equal to <paramref name="searchNode"/></returns>
+ /// <exception cref="ArgumentNullException"><paramref name="searchNode"/> is null</exception>
+ public T GetNode(T searchNode)
+ {
+ ArgumentNullException.ThrowIfNull(searchNode);
+
+ T node = Root;
+ while (node != null)
+ {
+ int cmp = searchNode.CompareTo(node);
+ if (cmp < 0)
+ {
+ node = node.Left;
+ }
+ else if (cmp > 0)
+ {
+ node = node.Right;
+ }
+ else
+ {
+ return node;
+ }
+ }
+ return null;
+ }
+
+ #endregion
+
+ #region Private Methods (BST)
+
+ /// <summary>
+ /// Inserts a new node into the tree.
+ /// </summary>
+ /// <param name="node">Node to be inserted</param>
+ private void Insert(T node)
+ {
+ T newNode = BSTInsert(node);
+ RestoreBalanceAfterInsertion(newNode);
+ }
+
+ /// <summary>
+ /// Insertion Mechanism for a Binary Search Tree (BST).
+ /// <br></br>
+ /// Iterates the tree starting from the root and inserts a new node
+ /// where all children in the left subtree are less than <paramref name="newNode"/>,
+ /// and all children in the right subtree are greater than <paramref name="newNode"/>.
+ /// </summary>
+ /// <param name="newNode">Node to be inserted</param>
+ /// <returns>The inserted Node</returns>
+ private T BSTInsert(T newNode)
+ {
+ T parent = null;
+ T node = Root;
+
+ while (node != null)
+ {
+ parent = node;
+ int cmp = newNode.CompareTo(node);
+ if (cmp < 0)
+ {
+ node = node.Left;
+ }
+ else if (cmp > 0)
+ {
+ node = node.Right;
+ }
+ else
+ {
+ return node;
+ }
+ }
+ newNode.Parent = parent;
+ if (parent == null)
+ {
+ Root = newNode;
+ }
+ else if (newNode.CompareTo(parent) < 0)
+ {
+ parent.Left = newNode;
+ }
+ else
+ {
+ parent.Right = newNode;
+ }
+ Count++;
+ return newNode;
+ }
+
+ /// <summary>
+ /// Removes <paramref name="nodeToDelete"/> from the tree, if it exists.
+ /// </summary>
+ /// <param name="nodeToDelete">Node to be removed</param>
+ /// <returns>The deleted Node</returns>
+ private T Delete(T nodeToDelete)
+ {
+ if (nodeToDelete == null)
+ {
+ return null;
+ }
+
+ T old = nodeToDelete;
+ T child;
+ T parent;
+ bool color;
+
+ if (LeftOf(nodeToDelete) == null)
+ {
+ child = RightOf(nodeToDelete);
+ }
+ else if (RightOf(nodeToDelete) == null)
+ {
+ child = LeftOf(nodeToDelete);
+ }
+ else
+ {
+ T element = Minimum(RightOf(nodeToDelete));
+
+ child = RightOf(element);
+ parent = ParentOf(element);
+ color = ColorOf(element);
+
+ if (child != null)
+ {
+ child.Parent = parent;
+ }
+
+ if (parent == null)
+ {
+ Root = child;
+ }
+ else if (element == LeftOf(parent))
+ {
+ parent.Left = child;
+ }
+ else
+ {
+ parent.Right = child;
+ }
+
+ if (ParentOf(element) == old)
+ {
+ parent = element;
+ }
+
+ element.Color = old.Color;
+ element.Left = old.Left;
+ element.Right = old.Right;
+ element.Parent = old.Parent;
+
+ if (ParentOf(old) == null)
+ {
+ Root = element;
+ }
+ else if (old == LeftOf(ParentOf(old)))
+ {
+ ParentOf(old).Left = element;
+ }
+ else
+ {
+ ParentOf(old).Right = element;
+ }
+
+ LeftOf(old).Parent = element;
+
+ if (RightOf(old) != null)
+ {
+ RightOf(old).Parent = element;
+ }
+
+ if (child != null && color == Black)
+ {
+ RestoreBalanceAfterRemoval(child);
+ }
+
+ return old;
+ }
+
+ parent = ParentOf(nodeToDelete);
+ color = ColorOf(nodeToDelete);
+
+ if (child != null)
+ {
+ child.Parent = parent;
+ }
+
+ if (parent == null)
+ {
+ Root = child;
+ }
+ else if (nodeToDelete == LeftOf(parent))
+ {
+ parent.Left = child;
+ }
+ else
+ {
+ parent.Right = child;
+ }
+
+ if (child != null && color == Black)
+ {
+ RestoreBalanceAfterRemoval(child);
+ }
+
+ return old;
+ }
+
+ #endregion
+ }
+
+ public static class IntrusiveRedBlackTreeExtensions
+ {
+ /// <summary>
+ /// Retrieve the node that is considered equal to the key by the comparator.
+ /// </summary>
+ /// <param name="tree">Tree to search at</param>
+ /// <param name="key">Key of the node to be found</param>
+ /// <returns>Node that is equal to <paramref name="key"/></returns>
+ public static N GetNodeByKey<N, K>(this IntrusiveRedBlackTree<N> tree, K key)
+ where N : IntrusiveRedBlackTreeNode<N>, IComparable<N>, IComparable<K>
+ where K : struct
+ {
+ N node = tree.RootNode;
+ while (node != null)
+ {
+ int cmp = node.CompareTo(key);
+ if (cmp < 0)
+ {
+ node = node.Right;
+ }
+ else if (cmp > 0)
+ {
+ node = node.Left;
+ }
+ else
+ {
+ return node;
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Collections/IntrusiveRedBlackTreeImpl.cs b/src/Ryujinx.Common/Collections/IntrusiveRedBlackTreeImpl.cs
new file mode 100644
index 00000000..bcb2e2a2
--- /dev/null
+++ b/src/Ryujinx.Common/Collections/IntrusiveRedBlackTreeImpl.cs
@@ -0,0 +1,354 @@
+using System;
+
+namespace Ryujinx.Common.Collections
+{
+ /// <summary>
+ /// Tree that provides the ability for O(logN) lookups for keys that exist in the tree, and O(logN) lookups for keys immediately greater than or less than a specified key.
+ /// </summary>
+ /// <typeparam name="T">Derived node type</typeparam>
+ public class IntrusiveRedBlackTreeImpl<T> where T : IntrusiveRedBlackTreeNode<T>
+ {
+ protected const bool Black = true;
+ protected const bool Red = false;
+ protected T Root = null;
+
+ internal T RootNode => Root;
+
+ /// <summary>
+ /// Number of nodes on the tree.
+ /// </summary>
+ public int Count { get; protected set; }
+
+ /// <summary>
+ /// Removes all nodes on the tree.
+ /// </summary>
+ public void Clear()
+ {
+ Root = null;
+ Count = 0;
+ }
+
+ /// <summary>
+ /// Finds the node whose key is immediately greater than <paramref name="node"/>.
+ /// </summary>
+ /// <param name="node">Node to find the successor of</param>
+ /// <returns>Successor of <paramref name="node"/></returns>
+ internal static T SuccessorOf(T node)
+ {
+ if (node.Right != null)
+ {
+ return Minimum(node.Right);
+ }
+ T parent = node.Parent;
+ while (parent != null && node == parent.Right)
+ {
+ node = parent;
+ parent = parent.Parent;
+ }
+ return parent;
+ }
+
+ /// <summary>
+ /// Finds the node whose key is immediately less than <paramref name="node"/>.
+ /// </summary>
+ /// <param name="node">Node to find the predecessor of</param>
+ /// <returns>Predecessor of <paramref name="node"/></returns>
+ internal static T PredecessorOf(T node)
+ {
+ if (node.Left != null)
+ {
+ return Maximum(node.Left);
+ }
+ T parent = node.Parent;
+ while (parent != null && node == parent.Left)
+ {
+ node = parent;
+ parent = parent.Parent;
+ }
+ return parent;
+ }
+
+ /// <summary>
+ /// Returns the node with the largest key where <paramref name="node"/> is considered the root node.
+ /// </summary>
+ /// <param name="node">Root node</param>
+ /// <returns>Node with the maximum key in the tree of <paramref name="node"/></returns>
+ protected static T Maximum(T node)
+ {
+ T tmp = node;
+ while (tmp.Right != null)
+ {
+ tmp = tmp.Right;
+ }
+
+ return tmp;
+ }
+
+ /// <summary>
+ /// Returns the node with the smallest key where <paramref name="node"/> is considered the root node.
+ /// </summary>
+ /// <param name="node">Root node</param>
+ /// <returns>Node with the minimum key in the tree of <paramref name="node"/></returns>
+ /// <exception cref="ArgumentNullException"><paramref name="node"/> is null</exception>
+ protected static T Minimum(T node)
+ {
+ ArgumentNullException.ThrowIfNull(node);
+
+ T tmp = node;
+ while (tmp.Left != null)
+ {
+ tmp = tmp.Left;
+ }
+
+ return tmp;
+ }
+
+ protected void RestoreBalanceAfterRemoval(T balanceNode)
+ {
+ T ptr = balanceNode;
+
+ while (ptr != Root && ColorOf(ptr) == Black)
+ {
+ if (ptr == LeftOf(ParentOf(ptr)))
+ {
+ T sibling = RightOf(ParentOf(ptr));
+
+ if (ColorOf(sibling) == Red)
+ {
+ SetColor(sibling, Black);
+ SetColor(ParentOf(ptr), Red);
+ RotateLeft(ParentOf(ptr));
+ sibling = RightOf(ParentOf(ptr));
+ }
+ if (ColorOf(LeftOf(sibling)) == Black && ColorOf(RightOf(sibling)) == Black)
+ {
+ SetColor(sibling, Red);
+ ptr = ParentOf(ptr);
+ }
+ else
+ {
+ if (ColorOf(RightOf(sibling)) == Black)
+ {
+ SetColor(LeftOf(sibling), Black);
+ SetColor(sibling, Red);
+ RotateRight(sibling);
+ sibling = RightOf(ParentOf(ptr));
+ }
+ SetColor(sibling, ColorOf(ParentOf(ptr)));
+ SetColor(ParentOf(ptr), Black);
+ SetColor(RightOf(sibling), Black);
+ RotateLeft(ParentOf(ptr));
+ ptr = Root;
+ }
+ }
+ else
+ {
+ T sibling = LeftOf(ParentOf(ptr));
+
+ if (ColorOf(sibling) == Red)
+ {
+ SetColor(sibling, Black);
+ SetColor(ParentOf(ptr), Red);
+ RotateRight(ParentOf(ptr));
+ sibling = LeftOf(ParentOf(ptr));
+ }
+ if (ColorOf(RightOf(sibling)) == Black && ColorOf(LeftOf(sibling)) == Black)
+ {
+ SetColor(sibling, Red);
+ ptr = ParentOf(ptr);
+ }
+ else
+ {
+ if (ColorOf(LeftOf(sibling)) == Black)
+ {
+ SetColor(RightOf(sibling), Black);
+ SetColor(sibling, Red);
+ RotateLeft(sibling);
+ sibling = LeftOf(ParentOf(ptr));
+ }
+ SetColor(sibling, ColorOf(ParentOf(ptr)));
+ SetColor(ParentOf(ptr), Black);
+ SetColor(LeftOf(sibling), Black);
+ RotateRight(ParentOf(ptr));
+ ptr = Root;
+ }
+ }
+ }
+ SetColor(ptr, Black);
+ }
+
+ protected void RestoreBalanceAfterInsertion(T balanceNode)
+ {
+ SetColor(balanceNode, Red);
+ while (balanceNode != null && balanceNode != Root && ColorOf(ParentOf(balanceNode)) == Red)
+ {
+ if (ParentOf(balanceNode) == LeftOf(ParentOf(ParentOf(balanceNode))))
+ {
+ T sibling = RightOf(ParentOf(ParentOf(balanceNode)));
+
+ if (ColorOf(sibling) == Red)
+ {
+ SetColor(ParentOf(balanceNode), Black);
+ SetColor(sibling, Black);
+ SetColor(ParentOf(ParentOf(balanceNode)), Red);
+ balanceNode = ParentOf(ParentOf(balanceNode));
+ }
+ else
+ {
+ if (balanceNode == RightOf(ParentOf(balanceNode)))
+ {
+ balanceNode = ParentOf(balanceNode);
+ RotateLeft(balanceNode);
+ }
+ SetColor(ParentOf(balanceNode), Black);
+ SetColor(ParentOf(ParentOf(balanceNode)), Red);
+ RotateRight(ParentOf(ParentOf(balanceNode)));
+ }
+ }
+ else
+ {
+ T sibling = LeftOf(ParentOf(ParentOf(balanceNode)));
+
+ if (ColorOf(sibling) == Red)
+ {
+ SetColor(ParentOf(balanceNode), Black);
+ SetColor(sibling, Black);
+ SetColor(ParentOf(ParentOf(balanceNode)), Red);
+ balanceNode = ParentOf(ParentOf(balanceNode));
+ }
+ else
+ {
+ if (balanceNode == LeftOf(ParentOf(balanceNode)))
+ {
+ balanceNode = ParentOf(balanceNode);
+ RotateRight(balanceNode);
+ }
+ SetColor(ParentOf(balanceNode), Black);
+ SetColor(ParentOf(ParentOf(balanceNode)), Red);
+ RotateLeft(ParentOf(ParentOf(balanceNode)));
+ }
+ }
+ }
+ SetColor(Root, Black);
+ }
+
+ protected virtual void RotateLeft(T node)
+ {
+ if (node != null)
+ {
+ T right = RightOf(node);
+ node.Right = LeftOf(right);
+ if (node.Right != null)
+ {
+ node.Right.Parent = node;
+ }
+ T nodeParent = ParentOf(node);
+ right.Parent = nodeParent;
+ if (nodeParent == null)
+ {
+ Root = right;
+ }
+ else if (node == LeftOf(nodeParent))
+ {
+ nodeParent.Left = right;
+ }
+ else
+ {
+ nodeParent.Right = right;
+ }
+ right.Left = node;
+ node.Parent = right;
+ }
+ }
+
+ protected virtual void RotateRight(T node)
+ {
+ if (node != null)
+ {
+ T left = LeftOf(node);
+ node.Left = RightOf(left);
+ if (node.Left != null)
+ {
+ node.Left.Parent = node;
+ }
+ T nodeParent = ParentOf(node);
+ left.Parent = nodeParent;
+ if (nodeParent == null)
+ {
+ Root = left;
+ }
+ else if (node == RightOf(nodeParent))
+ {
+ nodeParent.Right = left;
+ }
+ else
+ {
+ nodeParent.Left = left;
+ }
+ left.Right = node;
+ node.Parent = left;
+ }
+ }
+
+ #region Safety-Methods
+
+ // These methods save memory by allowing us to forego sentinel nil nodes, as well as serve as protection against NullReferenceExceptions.
+
+ /// <summary>
+ /// Returns the color of <paramref name="node"/>, or Black if it is null.
+ /// </summary>
+ /// <param name="node">Node</param>
+ /// <returns>The boolean color of <paramref name="node"/>, or black if null</returns>
+ protected static bool ColorOf(T node)
+ {
+ return node == null || node.Color;
+ }
+
+ /// <summary>
+ /// Sets the color of <paramref name="node"/> node to <paramref name="color"/>.
+ /// <br></br>
+ /// This method does nothing if <paramref name="node"/> is null.
+ /// </summary>
+ /// <param name="node">Node to set the color of</param>
+ /// <param name="color">Color (Boolean)</param>
+ protected static void SetColor(T node, bool color)
+ {
+ if (node != null)
+ {
+ node.Color = color;
+ }
+ }
+
+ /// <summary>
+ /// This method returns the left node of <paramref name="node"/>, or null if <paramref name="node"/> is null.
+ /// </summary>
+ /// <param name="node">Node to retrieve the left child from</param>
+ /// <returns>Left child of <paramref name="node"/></returns>
+ protected static T LeftOf(T node)
+ {
+ return node?.Left;
+ }
+
+ /// <summary>
+ /// This method returns the right node of <paramref name="node"/>, or null if <paramref name="node"/> is null.
+ /// </summary>
+ /// <param name="node">Node to retrieve the right child from</param>
+ /// <returns>Right child of <paramref name="node"/></returns>
+ protected static T RightOf(T node)
+ {
+ return node?.Right;
+ }
+
+ /// <summary>
+ /// Returns the parent node of <paramref name="node"/>, or null if <paramref name="node"/> is null.
+ /// </summary>
+ /// <param name="node">Node to retrieve the parent from</param>
+ /// <returns>Parent of <paramref name="node"/></returns>
+ protected static T ParentOf(T node)
+ {
+ return node?.Parent;
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Ryujinx.Common/Collections/IntrusiveRedBlackTreeNode.cs b/src/Ryujinx.Common/Collections/IntrusiveRedBlackTreeNode.cs
new file mode 100644
index 00000000..7143240d
--- /dev/null
+++ b/src/Ryujinx.Common/Collections/IntrusiveRedBlackTreeNode.cs
@@ -0,0 +1,16 @@
+namespace Ryujinx.Common.Collections
+{
+ /// <summary>
+ /// Represents a node in the Red-Black Tree.
+ /// </summary>
+ public class IntrusiveRedBlackTreeNode<T> where T : IntrusiveRedBlackTreeNode<T>
+ {
+ internal bool Color = true;
+ internal T Left;
+ internal T Right;
+ internal T Parent;
+
+ public T Predecessor => IntrusiveRedBlackTreeImpl<T>.PredecessorOf((T)this);
+ public T Successor => IntrusiveRedBlackTreeImpl<T>.SuccessorOf((T)this);
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Collections/TreeDictionary.cs b/src/Ryujinx.Common/Collections/TreeDictionary.cs
new file mode 100644
index 00000000..d118a30c
--- /dev/null
+++ b/src/Ryujinx.Common/Collections/TreeDictionary.cs
@@ -0,0 +1,617 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Ryujinx.Common.Collections
+{
+ /// <summary>
+ /// Dictionary that provides the ability for O(logN) Lookups for keys that exist in the Dictionary, and O(logN) lookups for keys immediately greater than or less than a specified key.
+ /// </summary>
+ /// <typeparam name="K">Key</typeparam>
+ /// <typeparam name="V">Value</typeparam>
+ public class TreeDictionary<K, V> : IntrusiveRedBlackTreeImpl<Node<K, V>>, IDictionary<K, V> where K : IComparable<K>
+ {
+ #region Public Methods
+
+ /// <summary>
+ /// Returns the value of the node whose key is <paramref name="key"/>, or the default value if no such node exists.
+ /// </summary>
+ /// <param name="key">Key of the node value to get</param>
+ /// <returns>Value associated w/ <paramref name="key"/></returns>
+ /// <exception cref="ArgumentNullException"><paramref name="key"/> is null</exception>
+ public V Get(K key)
+ {
+ ArgumentNullException.ThrowIfNull(key);
+
+ Node<K, V> node = GetNode(key);
+
+ if (node == null)
+ {
+ return default;
+ }
+
+ return node.Value;
+ }
+
+ /// <summary>
+ /// Adds a new node into the tree whose key is <paramref name="key"/> key and value is <paramref name="value"/>.
+ /// <br></br>
+ /// <b>Note:</b> Adding the same key multiple times will cause the value for that key to be overwritten.
+ /// </summary>
+ /// <param name="key">Key of the node to add</param>
+ /// <param name="value">Value of the node to add</param>
+ /// <exception cref="ArgumentNullException"><paramref name="key"/> or <paramref name="value"/> are null</exception>
+ public void Add(K key, V value)
+ {
+ ArgumentNullException.ThrowIfNull(key);
+ ArgumentNullException.ThrowIfNull(value);
+
+ Insert(key, value);
+ }
+
+ /// <summary>
+ /// Removes the node whose key is <paramref name="key"/> from the tree.
+ /// </summary>
+ /// <param name="key">Key of the node to remove</param>
+ /// <exception cref="ArgumentNullException"><paramref name="key"/> is null</exception>
+ public void Remove(K key)
+ {
+ ArgumentNullException.ThrowIfNull(key);
+
+ if (Delete(key) != null)
+ {
+ Count--;
+ }
+ }
+
+ /// <summary>
+ /// Returns the value whose key is equal to or immediately less than <paramref name="key"/>.
+ /// </summary>
+ /// <param name="key">Key for which to find the floor value of</param>
+ /// <returns>Key of node immediately less than <paramref name="key"/></returns>
+ /// <exception cref="ArgumentNullException"><paramref name="key"/> is null</exception>
+ public K Floor(K key)
+ {
+ Node<K, V> node = FloorNode(key);
+ if (node != null)
+ {
+ return node.Key;
+ }
+ return default;
+ }
+
+ /// <summary>
+ /// Returns the node whose key is equal to or immediately greater than <paramref name="key"/>.
+ /// </summary>
+ /// <param name="key">Key for which to find the ceiling node of</param>
+ /// <returns>Key of node immediately greater than <paramref name="key"/></returns>
+ /// <exception cref="ArgumentNullException"><paramref name="key"/> is null</exception>
+ public K Ceiling(K key)
+ {
+ Node<K, V> node = CeilingNode(key);
+ if (node != null)
+ {
+ return node.Key;
+ }
+ return default;
+ }
+
+ /// <summary>
+ /// Finds the value whose key is immediately greater than <paramref name="key"/>.
+ /// </summary>
+ /// <param name="key">Key to find the successor of</param>
+ /// <returns>Value</returns>
+ public K SuccessorOf(K key)
+ {
+ Node<K, V> node = GetNode(key);
+ if (node != null)
+ {
+ Node<K, V> successor = SuccessorOf(node);
+
+ return successor != null ? successor.Key : default;
+ }
+ return default;
+ }
+
+ /// <summary>
+ /// Finds the value whose key is immediately less than <paramref name="key"/>.
+ /// </summary>
+ /// <param name="key">Key to find the predecessor of</param>
+ /// <returns>Value</returns>
+ public K PredecessorOf(K key)
+ {
+ Node<K, V> node = GetNode(key);
+ if (node != null)
+ {
+ Node<K, V> predecessor = PredecessorOf(node);
+
+ return predecessor != null ? predecessor.Key : default;
+ }
+ return default;
+ }
+
+ /// <summary>
+ /// Adds all the nodes in the dictionary as key/value pairs into <paramref name="list"/>.
+ /// <br></br>
+ /// The key/value pairs will be added in Level Order.
+ /// </summary>
+ /// <param name="list">List to add the tree pairs into</param>
+ public List<KeyValuePair<K, V>> AsLevelOrderList()
+ {
+ List<KeyValuePair<K, V>> list = new List<KeyValuePair<K, V>>();
+
+ Queue<Node<K, V>> nodes = new Queue<Node<K, V>>();
+
+ if (this.Root != null)
+ {
+ nodes.Enqueue(this.Root);
+ }
+ while (nodes.TryDequeue(out Node<K, V> node))
+ {
+ list.Add(new KeyValuePair<K, V>(node.Key, node.Value));
+ if (node.Left != null)
+ {
+ nodes.Enqueue(node.Left);
+ }
+ if (node.Right != null)
+ {
+ nodes.Enqueue(node.Right);
+ }
+ }
+ return list;
+ }
+
+ /// <summary>
+ /// Adds all the nodes in the dictionary into <paramref name="list"/>.
+ /// </summary>
+ /// <returns>A list of all KeyValuePairs sorted by Key Order</returns>
+ public List<KeyValuePair<K, V>> AsList()
+ {
+ List<KeyValuePair<K, V>> list = new List<KeyValuePair<K, V>>();
+
+ AddToList(Root, list);
+
+ return list;
+ }
+
+ #endregion
+
+ #region Private Methods (BST)
+
+ /// <summary>
+ /// Adds all nodes that are children of or contained within <paramref name="node"/> into <paramref name="list"/>, in Key Order.
+ /// </summary>
+ /// <param name="node">The node to search for nodes within</param>
+ /// <param name="list">The list to add node to</param>
+ private void AddToList(Node<K, V> node, List<KeyValuePair<K, V>> list)
+ {
+ if (node == null)
+ {
+ return;
+ }
+
+ AddToList(node.Left, list);
+
+ list.Add(new KeyValuePair<K, V>(node.Key, node.Value));
+
+ AddToList(node.Right, list);
+ }
+
+ /// <summary>
+ /// Retrieve the node reference whose key is <paramref name="key"/>, or null if no such node exists.
+ /// </summary>
+ /// <param name="key">Key of the node to get</param>
+ /// <returns>Node reference in the tree</returns>
+ /// <exception cref="ArgumentNullException"><paramref name="key"/> is null</exception>
+ private Node<K, V> GetNode(K key)
+ {
+ ArgumentNullException.ThrowIfNull(key);
+
+ Node<K, V> node = Root;
+ while (node != null)
+ {
+ int cmp = key.CompareTo(node.Key);
+ if (cmp < 0)
+ {
+ node = node.Left;
+ }
+ else if (cmp > 0)
+ {
+ node = node.Right;
+ }
+ else
+ {
+ return node;
+ }
+ }
+ return null;
+ }
+
+ /// <summary>
+ /// Inserts a new node into the tree whose key is <paramref name="key"/> and value is <paramref name="value"/>.
+ /// <br></br>
+ /// Adding the same key multiple times will overwrite the previous value.
+ /// </summary>
+ /// <param name="key">Key of the node to insert</param>
+ /// <param name="value">Value of the node to insert</param>
+ private void Insert(K key, V value)
+ {
+ Node<K, V> newNode = BSTInsert(key, value);
+ RestoreBalanceAfterInsertion(newNode);
+ }
+
+ /// <summary>
+ /// Insertion Mechanism for a Binary Search Tree (BST).
+ /// <br></br>
+ /// Iterates the tree starting from the root and inserts a new node where all children in the left subtree are less than <paramref name="key"/>, and all children in the right subtree are greater than <paramref name="key"/>.
+ /// <br></br>
+ /// <b>Note: </b> If a node whose key is <paramref name="key"/> already exists, it's value will be overwritten.
+ /// </summary>
+ /// <param name="key">Key of the node to insert</param>
+ /// <param name="value">Value of the node to insert</param>
+ /// <returns>The inserted Node</returns>
+ private Node<K, V> BSTInsert(K key, V value)
+ {
+ Node<K, V> parent = null;
+ Node<K, V> node = Root;
+
+ while (node != null)
+ {
+ parent = node;
+ int cmp = key.CompareTo(node.Key);
+ if (cmp < 0)
+ {
+ node = node.Left;
+ }
+ else if (cmp > 0)
+ {
+ node = node.Right;
+ }
+ else
+ {
+ node.Value = value;
+ return node;
+ }
+ }
+ Node<K, V> newNode = new Node<K, V>(key, value, parent);
+ if (newNode.Parent == null)
+ {
+ Root = newNode;
+ }
+ else if (key.CompareTo(parent.Key) < 0)
+ {
+ parent.Left = newNode;
+ }
+ else
+ {
+ parent.Right = newNode;
+ }
+ Count++;
+ return newNode;
+ }
+
+ /// <summary>
+ /// Removes <paramref name="key"/> from the dictionary, if it exists.
+ /// </summary>
+ /// <param name="key">Key of the node to delete</param>
+ /// <returns>The deleted Node</returns>
+ private Node<K, V> Delete(K key)
+ {
+ // O(1) Retrieval
+ Node<K, V> nodeToDelete = GetNode(key);
+
+ if (nodeToDelete == null) return null;
+
+ Node<K, V> replacementNode;
+
+ if (LeftOf(nodeToDelete) == null || RightOf(nodeToDelete) == null)
+ {
+ replacementNode = nodeToDelete;
+ }
+ else
+ {
+ replacementNode = PredecessorOf(nodeToDelete);
+ }
+
+ Node<K, V> tmp = LeftOf(replacementNode) ?? RightOf(replacementNode);
+
+ if (tmp != null)
+ {
+ tmp.Parent = ParentOf(replacementNode);
+ }
+
+ if (ParentOf(replacementNode) == null)
+ {
+ Root = tmp;
+ }
+ else if (replacementNode == LeftOf(ParentOf(replacementNode)))
+ {
+ ParentOf(replacementNode).Left = tmp;
+ }
+ else
+ {
+ ParentOf(replacementNode).Right = tmp;
+ }
+
+ if (replacementNode != nodeToDelete)
+ {
+ nodeToDelete.Key = replacementNode.Key;
+ nodeToDelete.Value = replacementNode.Value;
+ }
+
+ if (tmp != null && ColorOf(replacementNode) == Black)
+ {
+ RestoreBalanceAfterRemoval(tmp);
+ }
+
+ return replacementNode;
+ }
+
+ /// <summary>
+ /// Returns the node whose key immediately less than or equal to <paramref name="key"/>.
+ /// </summary>
+ /// <param name="key">Key for which to find the floor node of</param>
+ /// <returns>Node whose key is immediately less than or equal to <paramref name="key"/>, or null if no such node is found.</returns>
+ /// <exception cref="ArgumentNullException"><paramref name="key"/> is null</exception>
+ private Node<K, V> FloorNode(K key)
+ {
+ ArgumentNullException.ThrowIfNull(key);
+
+ Node<K, V> tmp = Root;
+
+ while (tmp != null)
+ {
+ int cmp = key.CompareTo(tmp.Key);
+ if (cmp > 0)
+ {
+ if (tmp.Right != null)
+ {
+ tmp = tmp.Right;
+ }
+ else
+ {
+ return tmp;
+ }
+ }
+ else if (cmp < 0)
+ {
+ if (tmp.Left != null)
+ {
+ tmp = tmp.Left;
+ }
+ else
+ {
+ Node<K, V> parent = tmp.Parent;
+ Node<K, V> ptr = tmp;
+ while (parent != null && ptr == parent.Left)
+ {
+ ptr = parent;
+ parent = parent.Parent;
+ }
+ return parent;
+ }
+ }
+ else
+ {
+ return tmp;
+ }
+ }
+ return null;
+ }
+
+ /// <summary>
+ /// Returns the node whose key is immediately greater than or equal to than <paramref name="key"/>.
+ /// </summary>
+ /// <param name="key">Key for which to find the ceiling node of</param>
+ /// <returns>Node whose key is immediately greater than or equal to <paramref name="key"/>, or null if no such node is found.</returns>
+ /// <exception cref="ArgumentNullException"><paramref name="key"/> is null</exception>
+ private Node<K, V> CeilingNode(K key)
+ {
+ ArgumentNullException.ThrowIfNull(key);
+
+ Node<K, V> tmp = Root;
+
+ while (tmp != null)
+ {
+ int cmp = key.CompareTo(tmp.Key);
+ if (cmp < 0)
+ {
+ if (tmp.Left != null)
+ {
+ tmp = tmp.Left;
+ }
+ else
+ {
+ return tmp;
+ }
+ }
+ else if (cmp > 0)
+ {
+ if (tmp.Right != null)
+ {
+ tmp = tmp.Right;
+ }
+ else
+ {
+ Node<K, V> parent = tmp.Parent;
+ Node<K, V> ptr = tmp;
+ while (parent != null && ptr == parent.Right)
+ {
+ ptr = parent;
+ parent = parent.Parent;
+ }
+ return parent;
+ }
+ }
+ else
+ {
+ return tmp;
+ }
+ }
+ return null;
+ }
+
+ #endregion
+
+ #region Interface Implementations
+
+ // Method descriptions are not provided as they are already included as part of the interface.
+ public bool ContainsKey(K key)
+ {
+ ArgumentNullException.ThrowIfNull(key);
+
+ return GetNode(key) != null;
+ }
+
+ bool IDictionary<K, V>.Remove(K key)
+ {
+ int count = Count;
+ Remove(key);
+ return count > Count;
+ }
+
+ public bool TryGetValue(K key, [MaybeNullWhen(false)] out V value)
+ {
+ ArgumentNullException.ThrowIfNull(key);
+
+ Node<K, V> node = GetNode(key);
+ value = node != null ? node.Value : default;
+ return node != null;
+ }
+
+ public void Add(KeyValuePair<K, V> item)
+ {
+ ArgumentNullException.ThrowIfNull(item.Key);
+
+ Add(item.Key, item.Value);
+ }
+
+ public bool Contains(KeyValuePair<K, V> item)
+ {
+ if (item.Key == null)
+ {
+ return false;
+ }
+
+ Node<K, V> node = GetNode(item.Key);
+ if (node != null)
+ {
+ return node.Key.Equals(item.Key) && node.Value.Equals(item.Value);
+ }
+ return false;
+ }
+
+ public void CopyTo(KeyValuePair<K, V>[] array, int arrayIndex)
+ {
+ if (arrayIndex < 0 || array.Length - arrayIndex < this.Count)
+ {
+ throw new ArgumentOutOfRangeException(nameof(arrayIndex));
+ }
+
+ SortedList<K, V> list = GetKeyValues();
+
+ int offset = 0;
+
+ for (int i = arrayIndex; i < array.Length && offset < list.Count; i++)
+ {
+ array[i] = new KeyValuePair<K, V>(list.Keys[i], list.Values[i]);
+ offset++;
+ }
+ }
+
+ public bool Remove(KeyValuePair<K, V> item)
+ {
+ Node<K, V> node = GetNode(item.Key);
+
+ if (node == null)
+ {
+ return false;
+ }
+
+ if (node.Value.Equals(item.Value))
+ {
+ int count = Count;
+ Remove(item.Key);
+ return count > Count;
+ }
+
+ return false;
+ }
+
+ public IEnumerator<KeyValuePair<K, V>> GetEnumerator()
+ {
+ return GetKeyValues().GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetKeyValues().GetEnumerator();
+ }
+
+ public ICollection<K> Keys => GetKeyValues().Keys;
+
+ public ICollection<V> Values => GetKeyValues().Values;
+
+ public bool IsReadOnly => false;
+
+ public V this[K key]
+ {
+ get => Get(key);
+ set => Add(key, value);
+ }
+
+ #endregion
+
+ #region Private Interface Helper Methods
+
+ /// <summary>
+ /// Returns a sorted list of all the node keys / values in the tree.
+ /// </summary>
+ /// <returns>List of node keys</returns>
+ private SortedList<K, V> GetKeyValues()
+ {
+ SortedList<K, V> set = new SortedList<K, V>();
+ Queue<Node<K, V>> queue = new Queue<Node<K, V>>();
+ if (Root != null)
+ {
+ queue.Enqueue(Root);
+ }
+
+ while (queue.TryDequeue(out Node<K, V> node))
+ {
+ set.Add(node.Key, node.Value);
+ if (null != node.Left)
+ {
+ queue.Enqueue(node.Left);
+ }
+ if (null != node.Right)
+ {
+ queue.Enqueue(node.Right);
+ }
+ }
+
+ return set;
+ }
+
+ #endregion
+ }
+
+ /// <summary>
+ /// Represents a node in the TreeDictionary which contains a key and value of generic type K and V, respectively.
+ /// </summary>
+ /// <typeparam name="K">Key of the node</typeparam>
+ /// <typeparam name="V">Value of the node</typeparam>
+ public class Node<K, V> : IntrusiveRedBlackTreeNode<Node<K, V>> where K : IComparable<K>
+ {
+ internal K Key;
+ internal V Value;
+
+ internal Node(K key, V value, Node<K, V> parent)
+ {
+ Key = key;
+ Value = value;
+ Parent = parent;
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Configuration/AntiAliasing.cs b/src/Ryujinx.Common/Configuration/AntiAliasing.cs
new file mode 100644
index 00000000..159108ae
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/AntiAliasing.cs
@@ -0,0 +1,16 @@
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration
+{
+ [JsonConverter(typeof(TypedStringEnumConverter<AntiAliasing>))]
+ public enum AntiAliasing
+ {
+ None,
+ Fxaa,
+ SmaaLow,
+ SmaaMedium,
+ SmaaHigh,
+ SmaaUltra
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Configuration/AppDataManager.cs b/src/Ryujinx.Common/Configuration/AppDataManager.cs
new file mode 100644
index 00000000..d6e77843
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/AppDataManager.cs
@@ -0,0 +1,149 @@
+using Ryujinx.Common.Logging;
+using System;
+using System.IO;
+
+namespace Ryujinx.Common.Configuration
+{
+ public static class AppDataManager
+ {
+ public const string DefaultBaseDir = "Ryujinx";
+ public const string DefaultPortableDir = "portable";
+
+ // The following 3 are always part of Base Directory
+ private const string GamesDir = "games";
+ private const string ProfilesDir = "profiles";
+ private const string KeysDir = "system";
+
+ public enum LaunchMode
+ {
+ UserProfile,
+ Portable,
+ Custom
+ }
+
+ public static LaunchMode Mode { get; private set; }
+
+ public static string BaseDirPath { get; private set; }
+ public static string GamesDirPath { get; private set; }
+ public static string ProfilesDirPath { get; private set; }
+ public static string KeysDirPath { get; private set; }
+ public static string KeysDirPathUser { get; }
+
+ public const string DefaultNandDir = "bis";
+ public const string DefaultSdcardDir = "sdcard";
+ private const string DefaultModsDir = "mods";
+
+ public static string CustomModsPath { get; set; }
+ public static string CustomSdModsPath {get; set; }
+ public static string CustomNandPath { get; set; } // TODO: Actually implement this into VFS
+ public static string CustomSdCardPath { get; set; } // TODO: Actually implement this into VFS
+
+ static AppDataManager()
+ {
+ KeysDirPathUser = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".switch");
+ }
+
+ public static void Initialize(string baseDirPath)
+ {
+ string appDataPath;
+ if (OperatingSystem.IsMacOS())
+ {
+ appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Library", "Application Support");
+ }
+ else
+ {
+ appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
+ }
+
+ if (appDataPath.Length == 0)
+ {
+ appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+ }
+
+ string userProfilePath = Path.Combine(appDataPath, DefaultBaseDir);
+ string portablePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, DefaultPortableDir);
+
+ if (Directory.Exists(portablePath))
+ {
+ BaseDirPath = portablePath;
+ Mode = LaunchMode.Portable;
+ }
+ else
+ {
+ BaseDirPath = userProfilePath;
+ Mode = LaunchMode.UserProfile;
+ }
+
+ if (baseDirPath != null && baseDirPath != userProfilePath)
+ {
+ if (!Directory.Exists(baseDirPath))
+ {
+ Logger.Error?.Print(LogClass.Application, $"Custom Data Directory '{baseDirPath}' does not exist. Falling back to {Mode}...");
+ }
+ else
+ {
+ BaseDirPath = baseDirPath;
+ Mode = LaunchMode.Custom;
+ }
+ }
+
+ BaseDirPath = Path.GetFullPath(BaseDirPath); // convert relative paths
+
+ // NOTE: Moves the Ryujinx folder in `~/.config` to `~/Library/Application Support` if one is found
+ // and a Ryujinx folder does not already exist in Application Support.
+ // Also creates a symlink from `~/.config/Ryujinx` to `~/Library/Application Support/Ryujinx` to preserve backwards compatibility.
+ // This should be removed in the future.
+ if (OperatingSystem.IsMacOS() && Mode == LaunchMode.UserProfile)
+ {
+ string oldConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), DefaultBaseDir);
+ if (Path.Exists(oldConfigPath) && !Path.Exists(BaseDirPath))
+ {
+ CopyDirectory(oldConfigPath, BaseDirPath);
+ Directory.Delete(oldConfigPath, true);
+ Directory.CreateSymbolicLink(oldConfigPath, BaseDirPath);
+ }
+ }
+
+ SetupBasePaths();
+ }
+
+ private static void SetupBasePaths()
+ {
+ Directory.CreateDirectory(BaseDirPath);
+ Directory.CreateDirectory(GamesDirPath = Path.Combine(BaseDirPath, GamesDir));
+ Directory.CreateDirectory(ProfilesDirPath = Path.Combine(BaseDirPath, ProfilesDir));
+ Directory.CreateDirectory(KeysDirPath = Path.Combine(BaseDirPath, KeysDir));
+ }
+
+ private static void CopyDirectory(string sourceDir, string destinationDir)
+ {
+ var dir = new DirectoryInfo(sourceDir);
+
+ if (!dir.Exists)
+ {
+ throw new DirectoryNotFoundException($"Source directory not found: {dir.FullName}");
+ }
+
+ DirectoryInfo[] subDirs = dir.GetDirectories();
+ Directory.CreateDirectory(destinationDir);
+
+ foreach (FileInfo file in dir.GetFiles())
+ {
+ if (file.Name == ".DS_Store")
+ {
+ continue;
+ }
+
+ file.CopyTo(Path.Combine(destinationDir, file.Name));
+ }
+
+ foreach (DirectoryInfo subDir in subDirs)
+ {
+ CopyDirectory(subDir.FullName, Path.Combine(destinationDir, subDir.Name));
+ }
+ }
+
+ public static string GetModsPath() => CustomModsPath ?? Directory.CreateDirectory(Path.Combine(BaseDirPath, DefaultModsDir)).FullName;
+ public static string GetSdModsPath() => CustomSdModsPath ?? Directory.CreateDirectory(Path.Combine(BaseDirPath, DefaultSdcardDir, "atmosphere")).FullName;
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Configuration/AspectRatioExtensions.cs b/src/Ryujinx.Common/Configuration/AspectRatioExtensions.cs
new file mode 100644
index 00000000..5e97ed19
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/AspectRatioExtensions.cs
@@ -0,0 +1,63 @@
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration
+{
+ [JsonConverter(typeof(TypedStringEnumConverter<AspectRatio>))]
+ public enum AspectRatio
+ {
+ Fixed4x3,
+ Fixed16x9,
+ Fixed16x10,
+ Fixed21x9,
+ Fixed32x9,
+ Stretched
+ }
+
+ public static class AspectRatioExtensions
+ {
+ public static float ToFloat(this AspectRatio aspectRatio)
+ {
+ return aspectRatio.ToFloatX() / aspectRatio.ToFloatY();
+ }
+
+ public static float ToFloatX(this AspectRatio aspectRatio)
+ {
+ return aspectRatio switch
+ {
+ AspectRatio.Fixed4x3 => 4.0f,
+ AspectRatio.Fixed16x9 => 16.0f,
+ AspectRatio.Fixed16x10 => 16.0f,
+ AspectRatio.Fixed21x9 => 21.0f,
+ AspectRatio.Fixed32x9 => 32.0f,
+ _ => 16.0f
+ };
+ }
+
+ public static float ToFloatY(this AspectRatio aspectRatio)
+ {
+ return aspectRatio switch
+ {
+ AspectRatio.Fixed4x3 => 3.0f,
+ AspectRatio.Fixed16x9 => 9.0f,
+ AspectRatio.Fixed16x10 => 10.0f,
+ AspectRatio.Fixed21x9 => 9.0f,
+ AspectRatio.Fixed32x9 => 9.0f,
+ _ => 9.0f
+ };
+ }
+
+ public static string ToText(this AspectRatio aspectRatio)
+ {
+ return aspectRatio switch
+ {
+ AspectRatio.Fixed4x3 => "4:3",
+ AspectRatio.Fixed16x9 => "16:9",
+ AspectRatio.Fixed16x10 => "16:10",
+ AspectRatio.Fixed21x9 => "21:9",
+ AspectRatio.Fixed32x9 => "32:9",
+ _ => "Stretched"
+ };
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Configuration/BackendThreading.cs b/src/Ryujinx.Common/Configuration/BackendThreading.cs
new file mode 100644
index 00000000..8833b3f0
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/BackendThreading.cs
@@ -0,0 +1,13 @@
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration
+{
+ [JsonConverter(typeof(TypedStringEnumConverter<BackendThreading>))]
+ public enum BackendThreading
+ {
+ Auto,
+ Off,
+ On
+ }
+}
diff --git a/src/Ryujinx.Common/Configuration/DownloadableContentContainer.cs b/src/Ryujinx.Common/Configuration/DownloadableContentContainer.cs
new file mode 100644
index 00000000..b6ae2f3f
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/DownloadableContentContainer.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration
+{
+ public struct DownloadableContentContainer
+ {
+ [JsonPropertyName("path")]
+ public string ContainerPath { get; set; }
+ [JsonPropertyName("dlc_nca_list")]
+ public List<DownloadableContentNca> DownloadableContentNcaList { get; set; }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Configuration/DownloadableContentJsonSerializerContext.cs b/src/Ryujinx.Common/Configuration/DownloadableContentJsonSerializerContext.cs
new file mode 100644
index 00000000..132c45a4
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/DownloadableContentJsonSerializerContext.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration
+{
+ [JsonSourceGenerationOptions(WriteIndented = true)]
+ [JsonSerializable(typeof(List<DownloadableContentContainer>))]
+ public partial class DownloadableContentJsonSerializerContext : JsonSerializerContext
+ {
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Configuration/DownloadableContentNca.cs b/src/Ryujinx.Common/Configuration/DownloadableContentNca.cs
new file mode 100644
index 00000000..80b67300
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/DownloadableContentNca.cs
@@ -0,0 +1,14 @@
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration
+{
+ public struct DownloadableContentNca
+ {
+ [JsonPropertyName("path")]
+ public string FullPath { get; set; }
+ [JsonPropertyName("title_id")]
+ public ulong TitleId { get; set; }
+ [JsonPropertyName("is_enabled")]
+ public bool Enabled { get; set; }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Configuration/GraphicsBackend.cs b/src/Ryujinx.Common/Configuration/GraphicsBackend.cs
new file mode 100644
index 00000000..d74dd6e1
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/GraphicsBackend.cs
@@ -0,0 +1,12 @@
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration
+{
+ [JsonConverter(typeof(TypedStringEnumConverter<GraphicsBackend>))]
+ public enum GraphicsBackend
+ {
+ Vulkan,
+ OpenGl
+ }
+}
diff --git a/src/Ryujinx.Common/Configuration/GraphicsDebugLevel.cs b/src/Ryujinx.Common/Configuration/GraphicsDebugLevel.cs
new file mode 100644
index 00000000..ad12302a
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/GraphicsDebugLevel.cs
@@ -0,0 +1,14 @@
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration
+{
+ [JsonConverter(typeof(TypedStringEnumConverter<GraphicsDebugLevel>))]
+ public enum GraphicsDebugLevel
+ {
+ None,
+ Error,
+ Slowdowns,
+ All
+ }
+}
diff --git a/src/Ryujinx.Common/Configuration/Hid/Controller/GamepadInputId.cs b/src/Ryujinx.Common/Configuration/Hid/Controller/GamepadInputId.cs
new file mode 100644
index 00000000..ad1fa667
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/Controller/GamepadInputId.cs
@@ -0,0 +1,58 @@
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration.Hid.Controller
+{
+ [JsonConverter(typeof(TypedStringEnumConverter<GamepadInputId>))]
+ public enum GamepadInputId : byte
+ {
+ Unbound,
+ A,
+ B,
+ X,
+ Y,
+ LeftStick,
+ RightStick,
+ LeftShoulder,
+ RightShoulder,
+
+ // Likely axis
+ LeftTrigger,
+ // Likely axis
+ RightTrigger,
+
+ DpadUp,
+ DpadDown,
+ DpadLeft,
+ DpadRight,
+
+ // Special buttons
+
+ Minus,
+ Plus,
+
+ Back = Minus,
+ Start = Plus,
+
+ Guide,
+ Misc1,
+
+ // Xbox Elite paddle
+ Paddle1,
+ Paddle2,
+ Paddle3,
+ Paddle4,
+
+ // PS5 touchpad button
+ Touchpad,
+
+ // Virtual buttons for single joycon
+ SingleLeftTrigger0,
+ SingleRightTrigger0,
+
+ SingleLeftTrigger1,
+ SingleRightTrigger1,
+
+ Count
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Configuration/Hid/Controller/GenericControllerInputConfig.cs b/src/Ryujinx.Common/Configuration/Hid/Controller/GenericControllerInputConfig.cs
new file mode 100644
index 00000000..d7f0e788
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/Controller/GenericControllerInputConfig.cs
@@ -0,0 +1,82 @@
+using Ryujinx.Common.Configuration.Hid.Controller.Motion;
+using System;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration.Hid.Controller
+{
+ public class GenericControllerInputConfig<Button, Stick> : GenericInputConfigurationCommon<Button> where Button : unmanaged where Stick : unmanaged
+ {
+ [JsonIgnore]
+ private float _deadzoneLeft;
+ [JsonIgnore]
+ private float _deadzoneRight;
+ [JsonIgnore]
+ private float _triggerThreshold;
+
+ /// <summary>
+ /// Left JoyCon Controller Stick Bindings
+ /// </summary>
+ public JoyconConfigControllerStick<Button, Stick> LeftJoyconStick { get; set; }
+
+ /// <summary>
+ /// Right JoyCon Controller Stick Bindings
+ /// </summary>
+ public JoyconConfigControllerStick<Button, Stick> RightJoyconStick { get; set; }
+
+ /// <summary>
+ /// Controller Left Analog Stick Deadzone
+ /// </summary>
+ public float DeadzoneLeft
+ {
+ get => _deadzoneLeft; set
+ {
+ _deadzoneLeft = MathF.Round(value, 3);
+ OnPropertyChanged();
+ }
+ }
+
+ /// <summary>
+ /// Controller Right Analog Stick Deadzone
+ /// </summary>
+ public float DeadzoneRight
+ {
+ get => _deadzoneRight; set
+ {
+ _deadzoneRight = MathF.Round(value, 3);
+ OnPropertyChanged();
+ }
+ }
+
+ /// <summary>
+ /// Controller Left Analog Stick Range
+ /// </summary>
+ public float RangeLeft { get; set; }
+
+ /// <summary>
+ /// Controller Right Analog Stick Range
+ /// </summary>
+ public float RangeRight { get; set; }
+
+ /// <summary>
+ /// Controller Trigger Threshold
+ /// </summary>
+ public float TriggerThreshold
+ {
+ get => _triggerThreshold; set
+ {
+ _triggerThreshold = MathF.Round(value, 3);
+ OnPropertyChanged();
+ }
+ }
+
+ /// <summary>
+ /// Controller Motion Settings
+ /// </summary>
+ public MotionConfigController Motion { get; set; }
+
+ /// <summary>
+ /// Controller Rumble Settings
+ /// </summary>
+ public RumbleConfigController Rumble { get; set; }
+ }
+}
diff --git a/src/Ryujinx.Common/Configuration/Hid/Controller/JoyconConfigControllerStick.cs b/src/Ryujinx.Common/Configuration/Hid/Controller/JoyconConfigControllerStick.cs
new file mode 100644
index 00000000..869cff4f
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/Controller/JoyconConfigControllerStick.cs
@@ -0,0 +1,11 @@
+namespace Ryujinx.Common.Configuration.Hid.Controller
+{
+ public class JoyconConfigControllerStick<Button, Stick> where Button: unmanaged where Stick: unmanaged
+ {
+ public Stick Joystick { get; set; }
+ public bool InvertStickX { get; set; }
+ public bool InvertStickY { get; set; }
+ public bool Rotate90CW { get; set; }
+ public Button StickButton { get; set; }
+ }
+}
diff --git a/src/Ryujinx.Common/Configuration/Hid/Controller/Motion/CemuHookMotionConfigController.cs b/src/Ryujinx.Common/Configuration/Hid/Controller/Motion/CemuHookMotionConfigController.cs
new file mode 100644
index 00000000..2a5a73ff
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/Controller/Motion/CemuHookMotionConfigController.cs
@@ -0,0 +1,30 @@
+namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
+{
+ public class CemuHookMotionConfigController : MotionConfigController
+ {
+ /// <summary>
+ /// Motion Controller Slot
+ /// </summary>
+ public int Slot { get; set; }
+
+ /// <summary>
+ /// Motion Controller Alternative Slot, for RightJoyCon in Pair mode
+ /// </summary>
+ public int AltSlot { get; set; }
+
+ /// <summary>
+ /// Mirror motion input in Pair mode
+ /// </summary>
+ public bool MirrorInput { get; set; }
+
+ /// <summary>
+ /// Host address of the DSU Server
+ /// </summary>
+ public string DsuServerHost { get; set; }
+
+ /// <summary>
+ /// Port of the DSU Server
+ /// </summary>
+ public int DsuServerPort { get; set; }
+ }
+}
diff --git a/src/Ryujinx.Common/Configuration/Hid/Controller/Motion/JsonMotionConfigControllerConverter.cs b/src/Ryujinx.Common/Configuration/Hid/Controller/Motion/JsonMotionConfigControllerConverter.cs
new file mode 100644
index 00000000..2b9e0af4
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/Controller/Motion/JsonMotionConfigControllerConverter.cs
@@ -0,0 +1,79 @@
+using Ryujinx.Common.Utilities;
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
+{
+ class JsonMotionConfigControllerConverter : JsonConverter<MotionConfigController>
+ {
+ private static readonly MotionConfigJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
+
+ private static MotionInputBackendType GetMotionInputBackendType(ref Utf8JsonReader reader)
+ {
+ // Temporary reader to get the backend type
+ Utf8JsonReader tempReader = reader;
+
+ MotionInputBackendType result = MotionInputBackendType.Invalid;
+
+ while (tempReader.Read())
+ {
+ // NOTE: We scan all properties ignoring the depth entirely on purpose.
+ // The reason behind this is that we cannot track in a reliable way the depth of the object because Utf8JsonReader never emit the first TokenType == StartObject if the json start with an object.
+ // As such, this code will try to parse very field named "motion_backend" to the correct enum.
+ if (tempReader.TokenType == JsonTokenType.PropertyName)
+ {
+ string propertyName = tempReader.GetString();
+
+ if (propertyName.Equals("motion_backend"))
+ {
+ tempReader.Read();
+
+ if (tempReader.TokenType == JsonTokenType.String)
+ {
+ string backendTypeRaw = tempReader.GetString();
+
+ if (!Enum.TryParse(backendTypeRaw, out result))
+ {
+ result = MotionInputBackendType.Invalid;
+ }
+ else
+ {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ return result;
+ }
+
+ public override MotionConfigController Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ MotionInputBackendType motionBackendType = GetMotionInputBackendType(ref reader);
+
+ return motionBackendType switch
+ {
+ MotionInputBackendType.GamepadDriver => JsonSerializer.Deserialize(ref reader, SerializerContext.StandardMotionConfigController),
+ MotionInputBackendType.CemuHook => JsonSerializer.Deserialize(ref reader, SerializerContext.CemuHookMotionConfigController),
+ _ => throw new InvalidOperationException($"Unknown backend type {motionBackendType}"),
+ };
+ }
+
+ public override void Write(Utf8JsonWriter writer, MotionConfigController value, JsonSerializerOptions options)
+ {
+ switch (value.MotionBackend)
+ {
+ case MotionInputBackendType.GamepadDriver:
+ JsonSerializer.Serialize(writer, value as StandardMotionConfigController, SerializerContext.StandardMotionConfigController);
+ break;
+ case MotionInputBackendType.CemuHook:
+ JsonSerializer.Serialize(writer, value as CemuHookMotionConfigController, SerializerContext.CemuHookMotionConfigController);
+ break;
+ default:
+ throw new ArgumentException($"Unknown motion backend type {value.MotionBackend}");
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionConfigController.cs b/src/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionConfigController.cs
new file mode 100644
index 00000000..7636aa41
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionConfigController.cs
@@ -0,0 +1,25 @@
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
+{
+ [JsonConverter(typeof(JsonMotionConfigControllerConverter))]
+ public class MotionConfigController
+ {
+ public MotionInputBackendType MotionBackend { get; set; }
+
+ /// <summary>
+ /// Gyro Sensitivity
+ /// </summary>
+ public int Sensitivity { get; set; }
+
+ /// <summary>
+ /// Gyro Deadzone
+ /// </summary>
+ public double GyroDeadzone { get; set; }
+
+ /// <summary>
+ /// Enable Motion Controls
+ /// </summary>
+ public bool EnableMotion { get; set; }
+ }
+}
diff --git a/src/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionConfigJsonSerializerContext.cs b/src/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionConfigJsonSerializerContext.cs
new file mode 100644
index 00000000..5cd9e452
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionConfigJsonSerializerContext.cs
@@ -0,0 +1,12 @@
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
+{
+ [JsonSourceGenerationOptions(WriteIndented = true)]
+ [JsonSerializable(typeof(MotionConfigController))]
+ [JsonSerializable(typeof(CemuHookMotionConfigController))]
+ [JsonSerializable(typeof(StandardMotionConfigController))]
+ public partial class MotionConfigJsonSerializerContext : JsonSerializerContext
+ {
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionInputBackendType.cs b/src/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionInputBackendType.cs
new file mode 100644
index 00000000..c6551047
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionInputBackendType.cs
@@ -0,0 +1,13 @@
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
+{
+ [JsonConverter(typeof(TypedStringEnumConverter<MotionInputBackendType>))]
+ public enum MotionInputBackendType : byte
+ {
+ Invalid,
+ GamepadDriver,
+ CemuHook
+ }
+}
diff --git a/src/Ryujinx.Common/Configuration/Hid/Controller/Motion/StandardMotionConfigController.cs b/src/Ryujinx.Common/Configuration/Hid/Controller/Motion/StandardMotionConfigController.cs
new file mode 100644
index 00000000..df925444
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/Controller/Motion/StandardMotionConfigController.cs
@@ -0,0 +1,4 @@
+namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
+{
+ public class StandardMotionConfigController : MotionConfigController { }
+}
diff --git a/src/Ryujinx.Common/Configuration/Hid/Controller/RumbleConfigController.cs b/src/Ryujinx.Common/Configuration/Hid/Controller/RumbleConfigController.cs
new file mode 100644
index 00000000..48be4f13
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/Controller/RumbleConfigController.cs
@@ -0,0 +1,20 @@
+namespace Ryujinx.Common.Configuration.Hid.Controller
+{
+ public class RumbleConfigController
+ {
+ /// <summary>
+ /// Controller Strong Rumble Multiplier
+ /// </summary>
+ public float StrongRumble { get; set; }
+
+ /// <summary>
+ /// Controller Weak Rumble Multiplier
+ /// </summary>
+ public float WeakRumble { get; set; }
+
+ /// <summary>
+ /// Enable Rumble
+ /// </summary>
+ public bool EnableRumble { get; set; }
+ }
+}
diff --git a/src/Ryujinx.Common/Configuration/Hid/Controller/StandardControllerInputConfig.cs b/src/Ryujinx.Common/Configuration/Hid/Controller/StandardControllerInputConfig.cs
new file mode 100644
index 00000000..4154a42b
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/Controller/StandardControllerInputConfig.cs
@@ -0,0 +1,4 @@
+namespace Ryujinx.Common.Configuration.Hid.Controller
+{
+ public class StandardControllerInputConfig : GenericControllerInputConfig<GamepadInputId, StickInputId> { }
+}
diff --git a/src/Ryujinx.Common/Configuration/Hid/Controller/StickInputId.cs b/src/Ryujinx.Common/Configuration/Hid/Controller/StickInputId.cs
new file mode 100644
index 00000000..5fc4d1c8
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/Controller/StickInputId.cs
@@ -0,0 +1,15 @@
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration.Hid.Controller
+{
+ [JsonConverter(typeof(TypedStringEnumConverter<StickInputId>))]
+ public enum StickInputId : byte
+ {
+ Unbound,
+ Left,
+ Right,
+
+ Count
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Configuration/Hid/ControllerType.cs b/src/Ryujinx.Common/Configuration/Hid/ControllerType.cs
new file mode 100644
index 00000000..70f811c8
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/ControllerType.cs
@@ -0,0 +1,23 @@
+using Ryujinx.Common.Utilities;
+using System;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration.Hid
+{
+ // This enum was duplicated from Ryujinx.HLE.HOS.Services.Hid.PlayerIndex and should be kept identical
+ [Flags]
+ [JsonConverter(typeof(TypedStringEnumConverter<ControllerType>))]
+ public enum ControllerType : int
+ {
+ None,
+ ProController = 1 << 0,
+ Handheld = 1 << 1,
+ JoyconPair = 1 << 2,
+ JoyconLeft = 1 << 3,
+ JoyconRight = 1 << 4,
+ Invalid = 1 << 5,
+ Pokeball = 1 << 6,
+ SystemExternal = 1 << 29,
+ System = 1 << 30
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Configuration/Hid/GenericInputConfigurationCommon.cs b/src/Ryujinx.Common/Configuration/Hid/GenericInputConfigurationCommon.cs
new file mode 100644
index 00000000..3d43817e
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/GenericInputConfigurationCommon.cs
@@ -0,0 +1,15 @@
+namespace Ryujinx.Common.Configuration.Hid
+{
+ public class GenericInputConfigurationCommon<Button> : InputConfig where Button : unmanaged
+ {
+ /// <summary>
+ /// Left JoyCon Controller Bindings
+ /// </summary>
+ public LeftJoyconCommonConfig<Button> LeftJoycon { get; set; }
+
+ /// <summary>
+ /// Right JoyCon Controller Bindings
+ /// </summary>
+ public RightJoyconCommonConfig<Button> RightJoycon { get; set; }
+ }
+}
diff --git a/src/Ryujinx.Common/Configuration/Hid/InputBackendType.cs b/src/Ryujinx.Common/Configuration/Hid/InputBackendType.cs
new file mode 100644
index 00000000..1db3f570
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/InputBackendType.cs
@@ -0,0 +1,13 @@
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration.Hid
+{
+ [JsonConverter(typeof(TypedStringEnumConverter<InputBackendType>))]
+ public enum InputBackendType
+ {
+ Invalid,
+ WindowKeyboard,
+ GamepadSDL2,
+ }
+}
diff --git a/src/Ryujinx.Common/Configuration/Hid/InputConfig.cs b/src/Ryujinx.Common/Configuration/Hid/InputConfig.cs
new file mode 100644
index 00000000..16c8f8e3
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/InputConfig.cs
@@ -0,0 +1,41 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration.Hid
+{
+ [JsonConverter(typeof(JsonInputConfigConverter))]
+ public class InputConfig : INotifyPropertyChanged
+ {
+ /// <summary>
+ /// The current version of the input file format
+ /// </summary>
+ public const int CurrentVersion = 1;
+
+ public int Version { get; set; }
+
+ public InputBackendType Backend { get; set; }
+
+ /// <summary>
+ /// Controller id
+ /// </summary>
+ public string Id { get; set; }
+
+ /// <summary>
+ /// Controller's Type
+ /// </summary>
+ public ControllerType ControllerType { get; set; }
+
+ /// <summary>
+ /// Player's Index for the controller
+ /// </summary>
+ public PlayerIndex PlayerIndex { get; set; }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Configuration/Hid/InputConfigJsonSerializerContext.cs b/src/Ryujinx.Common/Configuration/Hid/InputConfigJsonSerializerContext.cs
new file mode 100644
index 00000000..254c4feb
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/InputConfigJsonSerializerContext.cs
@@ -0,0 +1,14 @@
+using Ryujinx.Common.Configuration.Hid.Controller;
+using Ryujinx.Common.Configuration.Hid.Keyboard;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration.Hid
+{
+ [JsonSourceGenerationOptions(WriteIndented = true)]
+ [JsonSerializable(typeof(InputConfig))]
+ [JsonSerializable(typeof(StandardKeyboardInputConfig))]
+ [JsonSerializable(typeof(StandardControllerInputConfig))]
+ public partial class InputConfigJsonSerializerContext : JsonSerializerContext
+ {
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Configuration/Hid/JsonInputConfigConverter.cs b/src/Ryujinx.Common/Configuration/Hid/JsonInputConfigConverter.cs
new file mode 100644
index 00000000..08bbcbf1
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/JsonInputConfigConverter.cs
@@ -0,0 +1,81 @@
+using Ryujinx.Common.Configuration.Hid.Controller;
+using Ryujinx.Common.Configuration.Hid.Keyboard;
+using Ryujinx.Common.Utilities;
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration.Hid
+{
+ public class JsonInputConfigConverter : JsonConverter<InputConfig>
+ {
+ private static readonly InputConfigJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
+
+ private static InputBackendType GetInputBackendType(ref Utf8JsonReader reader)
+ {
+ // Temporary reader to get the backend type
+ Utf8JsonReader tempReader = reader;
+
+ InputBackendType result = InputBackendType.Invalid;
+
+ while (tempReader.Read())
+ {
+ // NOTE: We scan all properties ignoring the depth entirely on purpose.
+ // The reason behind this is that we cannot track in a reliable way the depth of the object because Utf8JsonReader never emit the first TokenType == StartObject if the json start with an object.
+ // As such, this code will try to parse very field named "backend" to the correct enum.
+ if (tempReader.TokenType == JsonTokenType.PropertyName)
+ {
+ string propertyName = tempReader.GetString();
+
+ if (propertyName.Equals("backend"))
+ {
+ tempReader.Read();
+
+ if (tempReader.TokenType == JsonTokenType.String)
+ {
+ string backendTypeRaw = tempReader.GetString();
+
+ if (!Enum.TryParse(backendTypeRaw, out result))
+ {
+ result = InputBackendType.Invalid;
+ }
+ else
+ {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ return result;
+ }
+
+ public override InputConfig Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ InputBackendType backendType = GetInputBackendType(ref reader);
+
+ return backendType switch
+ {
+ InputBackendType.WindowKeyboard => JsonSerializer.Deserialize(ref reader, SerializerContext.StandardKeyboardInputConfig),
+ InputBackendType.GamepadSDL2 => JsonSerializer.Deserialize(ref reader, SerializerContext.StandardControllerInputConfig),
+ _ => throw new InvalidOperationException($"Unknown backend type {backendType}"),
+ };
+ }
+
+ public override void Write(Utf8JsonWriter writer, InputConfig value, JsonSerializerOptions options)
+ {
+ switch (value.Backend)
+ {
+ case InputBackendType.WindowKeyboard:
+ JsonSerializer.Serialize(writer, value as StandardKeyboardInputConfig, SerializerContext.StandardKeyboardInputConfig);
+ break;
+ case InputBackendType.GamepadSDL2:
+ JsonSerializer.Serialize(writer, value as StandardControllerInputConfig, SerializerContext.StandardControllerInputConfig);
+ break;
+ default:
+ throw new ArgumentException($"Unknown backend type {value.Backend}");
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Configuration/Hid/Key.cs b/src/Ryujinx.Common/Configuration/Hid/Key.cs
new file mode 100644
index 00000000..3501b8ae
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/Key.cs
@@ -0,0 +1,143 @@
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration.Hid
+{
+ [JsonConverter(typeof(TypedStringEnumConverter<Key>))]
+ public enum Key
+ {
+ Unknown,
+ ShiftLeft,
+ ShiftRight,
+ ControlLeft,
+ ControlRight,
+ AltLeft,
+ AltRight,
+ WinLeft,
+ WinRight,
+ Menu,
+ F1,
+ F2,
+ F3,
+ F4,
+ F5,
+ F6,
+ F7,
+ F8,
+ F9,
+ F10,
+ F11,
+ F12,
+ F13,
+ F14,
+ F15,
+ F16,
+ F17,
+ F18,
+ F19,
+ F20,
+ F21,
+ F22,
+ F23,
+ F24,
+ F25,
+ F26,
+ F27,
+ F28,
+ F29,
+ F30,
+ F31,
+ F32,
+ F33,
+ F34,
+ F35,
+ Up,
+ Down,
+ Left,
+ Right,
+ Enter,
+ Escape,
+ Space,
+ Tab,
+ BackSpace,
+ Insert,
+ Delete,
+ PageUp,
+ PageDown,
+ Home,
+ End,
+ CapsLock,
+ ScrollLock,
+ PrintScreen,
+ Pause,
+ NumLock,
+ Clear,
+ Keypad0,
+ Keypad1,
+ Keypad2,
+ Keypad3,
+ Keypad4,
+ Keypad5,
+ Keypad6,
+ Keypad7,
+ Keypad8,
+ Keypad9,
+ KeypadDivide,
+ KeypadMultiply,
+ KeypadSubtract,
+ KeypadAdd,
+ KeypadDecimal,
+ KeypadEnter,
+ A,
+ B,
+ C,
+ D,
+ E,
+ F,
+ G,
+ H,
+ I,
+ J,
+ K,
+ L,
+ M,
+ N,
+ O,
+ P,
+ Q,
+ R,
+ S,
+ T,
+ U,
+ V,
+ W,
+ X,
+ Y,
+ Z,
+ Number0,
+ Number1,
+ Number2,
+ Number3,
+ Number4,
+ Number5,
+ Number6,
+ Number7,
+ Number8,
+ Number9,
+ Tilde,
+ Grave,
+ Minus,
+ Plus,
+ BracketLeft,
+ BracketRight,
+ Semicolon,
+ Quote,
+ Comma,
+ Period,
+ Slash,
+ BackSlash,
+ Unbound,
+
+ Count
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Configuration/Hid/Keyboard/GenericKeyboardInputConfig.cs b/src/Ryujinx.Common/Configuration/Hid/Keyboard/GenericKeyboardInputConfig.cs
new file mode 100644
index 00000000..b6c82c93
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/Keyboard/GenericKeyboardInputConfig.cs
@@ -0,0 +1,15 @@
+namespace Ryujinx.Common.Configuration.Hid.Keyboard
+{
+ public class GenericKeyboardInputConfig<Key> : GenericInputConfigurationCommon<Key> where Key : unmanaged
+ {
+ /// <summary>
+ /// Left JoyCon Controller Stick Bindings
+ /// </summary>
+ public JoyconConfigKeyboardStick<Key> LeftJoyconStick { get; set; }
+
+ /// <summary>
+ /// Right JoyCon Controller Stick Bindings
+ /// </summary>
+ public JoyconConfigKeyboardStick<Key> RightJoyconStick { get; set; }
+ }
+}
diff --git a/src/Ryujinx.Common/Configuration/Hid/Keyboard/JoyconConfigKeyboardStick.cs b/src/Ryujinx.Common/Configuration/Hid/Keyboard/JoyconConfigKeyboardStick.cs
new file mode 100644
index 00000000..cadc17e8
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/Keyboard/JoyconConfigKeyboardStick.cs
@@ -0,0 +1,11 @@
+namespace Ryujinx.Common.Configuration.Hid.Keyboard
+{
+ public class JoyconConfigKeyboardStick<Key> where Key: unmanaged
+ {
+ public Key StickUp { get; set; }
+ public Key StickDown { get; set; }
+ public Key StickLeft { get; set; }
+ public Key StickRight { get; set; }
+ public Key StickButton { get; set; }
+ }
+}
diff --git a/src/Ryujinx.Common/Configuration/Hid/Keyboard/StandardKeyboardInputConfig.cs b/src/Ryujinx.Common/Configuration/Hid/Keyboard/StandardKeyboardInputConfig.cs
new file mode 100644
index 00000000..054d777d
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/Keyboard/StandardKeyboardInputConfig.cs
@@ -0,0 +1,4 @@
+namespace Ryujinx.Common.Configuration.Hid.Keyboard
+{
+ public class StandardKeyboardInputConfig : GenericKeyboardInputConfig<Key> { }
+}
diff --git a/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs b/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs
new file mode 100644
index 00000000..1a10c2a5
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs
@@ -0,0 +1,17 @@
+namespace Ryujinx.Common.Configuration.Hid
+{
+ // NOTE: Please don't change this to struct.
+ // This breaks Avalonia's TwoWay binding, which makes us unable to save new KeyboardHotkeys.
+ public class KeyboardHotkeys
+ {
+ public Key ToggleVsync { get; set; }
+ public Key Screenshot { get; set; }
+ public Key ShowUi { get; set; }
+ public Key Pause { get; set; }
+ public Key ToggleMute { get; set; }
+ public Key ResScaleUp { get; set; }
+ public Key ResScaleDown { get; set; }
+ public Key VolumeUp { get; set; }
+ public Key VolumeDown { get; set; }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Configuration/Hid/LeftJoyconCommonConfig.cs b/src/Ryujinx.Common/Configuration/Hid/LeftJoyconCommonConfig.cs
new file mode 100644
index 00000000..a57240c4
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/LeftJoyconCommonConfig.cs
@@ -0,0 +1,15 @@
+namespace Ryujinx.Common.Configuration.Hid
+{
+ public class LeftJoyconCommonConfig<Button>
+ {
+ public Button ButtonMinus { get; set; }
+ public Button ButtonL { get; set; }
+ public Button ButtonZl { get; set; }
+ public Button ButtonSl { get; set; }
+ public Button ButtonSr { get; set; }
+ public Button DpadUp { get; set; }
+ public Button DpadDown { get; set; }
+ public Button DpadLeft { get; set; }
+ public Button DpadRight { get; set; }
+ }
+}
diff --git a/src/Ryujinx.Common/Configuration/Hid/PlayerIndex.cs b/src/Ryujinx.Common/Configuration/Hid/PlayerIndex.cs
new file mode 100644
index 00000000..dd6495d4
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/PlayerIndex.cs
@@ -0,0 +1,22 @@
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration.Hid
+{
+ // This enum was duplicated from Ryujinx.HLE.HOS.Services.Hid.PlayerIndex and should be kept identical
+ [JsonConverter(typeof(TypedStringEnumConverter<PlayerIndex>))]
+ public enum PlayerIndex : int
+ {
+ Player1 = 0,
+ Player2 = 1,
+ Player3 = 2,
+ Player4 = 3,
+ Player5 = 4,
+ Player6 = 5,
+ Player7 = 6,
+ Player8 = 7,
+ Handheld = 8,
+ Unknown = 9,
+ Auto = 10 // Shouldn't be used directly
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Configuration/Hid/RightJoyconCommonConfig.cs b/src/Ryujinx.Common/Configuration/Hid/RightJoyconCommonConfig.cs
new file mode 100644
index 00000000..ca2d0176
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/RightJoyconCommonConfig.cs
@@ -0,0 +1,15 @@
+namespace Ryujinx.Common.Configuration.Hid
+{
+ public class RightJoyconCommonConfig<Button>
+ {
+ public Button ButtonPlus { get; set; }
+ public Button ButtonR { get; set; }
+ public Button ButtonZr { get; set; }
+ public Button ButtonSl { get; set; }
+ public Button ButtonSr { get; set; }
+ public Button ButtonX { get; set; }
+ public Button ButtonB { get; set; }
+ public Button ButtonY { get; set; }
+ public Button ButtonA { get; set; }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Configuration/MemoryManagerMode.cs b/src/Ryujinx.Common/Configuration/MemoryManagerMode.cs
new file mode 100644
index 00000000..f10fd6f1
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/MemoryManagerMode.cs
@@ -0,0 +1,13 @@
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration
+{
+ [JsonConverter(typeof(TypedStringEnumConverter<MemoryManagerMode>))]
+ public enum MemoryManagerMode : byte
+ {
+ SoftwarePageTable,
+ HostMapped,
+ HostMappedUnsafe
+ }
+}
diff --git a/src/Ryujinx.Common/Configuration/ScalingFilter.cs b/src/Ryujinx.Common/Configuration/ScalingFilter.cs
new file mode 100644
index 00000000..e38c7d73
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/ScalingFilter.cs
@@ -0,0 +1,13 @@
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration
+{
+ [JsonConverter(typeof(TypedStringEnumConverter<ScalingFilter>))]
+ public enum ScalingFilter
+ {
+ Bilinear,
+ Nearest,
+ Fsr
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Configuration/TitleUpdateMetadata.cs b/src/Ryujinx.Common/Configuration/TitleUpdateMetadata.cs
new file mode 100644
index 00000000..ea208e9c
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/TitleUpdateMetadata.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+
+namespace Ryujinx.Common.Configuration
+{
+ public struct TitleUpdateMetadata
+ {
+ public string Selected { get; set; }
+ public List<string> Paths { get; set; }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Configuration/TitleUpdateMetadataJsonSerializerContext.cs b/src/Ryujinx.Common/Configuration/TitleUpdateMetadataJsonSerializerContext.cs
new file mode 100644
index 00000000..5b661b87
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/TitleUpdateMetadataJsonSerializerContext.cs
@@ -0,0 +1,10 @@
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration
+{
+ [JsonSourceGenerationOptions(WriteIndented = true)]
+ [JsonSerializable(typeof(TitleUpdateMetadata))]
+ public partial class TitleUpdateMetadataJsonSerializerContext : JsonSerializerContext
+ {
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Extensions/BinaryReaderExtensions.cs b/src/Ryujinx.Common/Extensions/BinaryReaderExtensions.cs
new file mode 100644
index 00000000..21da6fc0
--- /dev/null
+++ b/src/Ryujinx.Common/Extensions/BinaryReaderExtensions.cs
@@ -0,0 +1,16 @@
+using System;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Common
+{
+ public static class BinaryReaderExtensions
+ {
+ public unsafe static T ReadStruct<T>(this BinaryReader reader)
+ where T : unmanaged
+ {
+ return MemoryMarshal.Cast<byte, T>(reader.ReadBytes(Unsafe.SizeOf<T>()))[0];
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Extensions/BinaryWriterExtensions.cs b/src/Ryujinx.Common/Extensions/BinaryWriterExtensions.cs
new file mode 100644
index 00000000..fddc8c1b
--- /dev/null
+++ b/src/Ryujinx.Common/Extensions/BinaryWriterExtensions.cs
@@ -0,0 +1,28 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Common
+{
+ public static class BinaryWriterExtensions
+ {
+ public unsafe static void WriteStruct<T>(this BinaryWriter writer, T value)
+ where T : unmanaged
+ {
+ ReadOnlySpan<byte> data = MemoryMarshal.Cast<T, byte>(MemoryMarshal.CreateReadOnlySpan(ref value, 1));
+
+ writer.Write(data);
+ }
+
+ public static void Write(this BinaryWriter writer, UInt128 value)
+ {
+ writer.Write((ulong)value);
+ writer.Write((ulong)(value >> 64));
+ }
+
+ public static void Write(this BinaryWriter writer, MemoryStream stream)
+ {
+ stream.CopyTo(writer.BaseStream);
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Extensions/StreamExtensions.cs b/src/Ryujinx.Common/Extensions/StreamExtensions.cs
new file mode 100644
index 00000000..f6fc870a
--- /dev/null
+++ b/src/Ryujinx.Common/Extensions/StreamExtensions.cs
@@ -0,0 +1,138 @@
+using System;
+using System.Buffers.Binary;
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Common
+{
+ public static class StreamExtensions
+ {
+ /// <summary>
+ /// Writes a <cref="ReadOnlySpan<int>" /> to this stream.
+ ///
+ /// This default implementation converts each buffer value to a stack-allocated
+ /// byte array, then writes it to the Stream using <cref="System.Stream.Write(byte[])" />.
+ /// </summary>
+ /// <param name="stream">The stream to be written to</param>
+ /// <param name="buffer">The buffer of values to be written</param>
+ public static void Write(this Stream stream, ReadOnlySpan<int> buffer)
+ {
+ if (buffer.Length == 0)
+ {
+ return;
+ }
+
+ if (BitConverter.IsLittleEndian)
+ {
+ ReadOnlySpan<byte> byteBuffer = MemoryMarshal.Cast<int, byte>(buffer);
+ stream.Write(byteBuffer);
+ }
+ else
+ {
+ Span<byte> byteBuffer = stackalloc byte[sizeof(int)];
+
+ foreach (int value in buffer)
+ {
+ BinaryPrimitives.WriteInt32LittleEndian(byteBuffer, value);
+ stream.Write(byteBuffer);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Writes a four-byte signed integer to this stream. The current position
+ /// of the stream is advanced by four.
+ /// </summary>
+ /// <param name="stream">The stream to be written to</param>
+ /// <param name="value">The value to be written</param>
+ public static void Write(this Stream stream, int value)
+ {
+ Span<byte> buffer = stackalloc byte[sizeof(int)];
+ BinaryPrimitives.WriteInt32LittleEndian(buffer, value);
+ stream.Write(buffer);
+ }
+
+ /// <summary>
+ /// Writes an eight-byte signed integer to this stream. The current position
+ /// of the stream is advanced by eight.
+ /// </summary>
+ /// <param name="stream">The stream to be written to</param>
+ /// <param name="value">The value to be written</param>
+ public static void Write(this Stream stream, long value)
+ {
+ Span<byte> buffer = stackalloc byte[sizeof(long)];
+ BinaryPrimitives.WriteInt64LittleEndian(buffer, value);
+ stream.Write(buffer);
+ }
+
+ /// <summary>
+ // Writes a four-byte unsigned integer to this stream. The current position
+ // of the stream is advanced by four.
+ /// </summary>
+ /// <param name="stream">The stream to be written to</param>
+ /// <param name="value">The value to be written</param>
+ public static void Write(this Stream stream, uint value)
+ {
+ Span<byte> buffer = stackalloc byte[sizeof(uint)];
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
+ stream.Write(buffer);
+ }
+
+ /// <summary>
+ /// Writes an eight-byte unsigned integer to this stream. The current
+ /// position of the stream is advanced by eight.
+ /// </summary>
+ /// <param name="stream">The stream to be written to</param>
+ /// <param name="value">The value to be written</param>
+ public static void Write(this Stream stream, ulong value)
+ {
+ Span<byte> buffer = stackalloc byte[sizeof(ulong)];
+ BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
+ stream.Write(buffer);
+ }
+
+ /// <summary>
+ /// Writes the contents of source to stream by calling source.CopyTo(stream).
+ /// Provides consistency with other Stream.Write methods.
+ /// </summary>
+ /// <param name="stream">The stream to be written to</param>
+ /// <param name="source">The stream to be read from</param>
+ public static void Write(this Stream stream, Stream source)
+ {
+ source.CopyTo(stream);
+ }
+
+ /// <summary>
+ /// Writes a sequence of bytes to the Stream.
+ /// </summary>
+ /// <param name="stream">The stream to be written to.</param>
+ /// <param name="value">The byte to be written</param>
+ /// <param name="count">The number of times the value should be written</param>
+ public static void WriteByte(this Stream stream, byte value, int count)
+ {
+ if (count <= 0)
+ {
+ return;
+ }
+
+ const int BlockSize = 16;
+
+ int blockCount = count / BlockSize;
+ if (blockCount > 0)
+ {
+ Span<byte> span = stackalloc byte[BlockSize];
+ span.Fill(value);
+ for (int x = 0; x < blockCount; x++)
+ {
+ stream.Write(span);
+ }
+ }
+
+ int nonBlockBytes = count % BlockSize;
+ for (int x = 0; x < nonBlockBytes; x++)
+ {
+ stream.WriteByte(value);
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/GraphicsDriver/DriverUtilities.cs b/src/Ryujinx.Common/GraphicsDriver/DriverUtilities.cs
new file mode 100644
index 00000000..3aabced7
--- /dev/null
+++ b/src/Ryujinx.Common/GraphicsDriver/DriverUtilities.cs
@@ -0,0 +1,22 @@
+using System;
+
+namespace Ryujinx.Common.GraphicsDriver
+{
+ public static class DriverUtilities
+ {
+ public static void ToggleOGLThreading(bool enabled)
+ {
+ Environment.SetEnvironmentVariable("mesa_glthread", enabled.ToString().ToLower());
+ Environment.SetEnvironmentVariable("__GL_THREADED_OPTIMIZATIONS", enabled ? "1" : "0");
+
+ try
+ {
+ NVThreadedOptimization.SetThreadedOptimization(enabled);
+ }
+ catch
+ {
+ // NVAPI is not available, or couldn't change the application profile.
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/GraphicsDriver/NVAPI/Nvapi.cs b/src/Ryujinx.Common/GraphicsDriver/NVAPI/Nvapi.cs
new file mode 100644
index 00000000..99eaa68f
--- /dev/null
+++ b/src/Ryujinx.Common/GraphicsDriver/NVAPI/Nvapi.cs
@@ -0,0 +1,11 @@
+namespace Ryujinx.Common.GraphicsDriver.NVAPI
+{
+ enum Nvapi : uint
+ {
+ OglThreadControlId = 0x20C1221E,
+
+ OglThreadControlDefault = 0,
+ OglThreadControlEnable = 1,
+ OglThreadControlDisable = 2
+ }
+}
diff --git a/src/Ryujinx.Common/GraphicsDriver/NVAPI/NvapiUnicodeString.cs b/src/Ryujinx.Common/GraphicsDriver/NVAPI/NvapiUnicodeString.cs
new file mode 100644
index 00000000..6bbff2de
--- /dev/null
+++ b/src/Ryujinx.Common/GraphicsDriver/NVAPI/NvapiUnicodeString.cs
@@ -0,0 +1,42 @@
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Ryujinx.Common.GraphicsDriver.NVAPI
+{
+ [StructLayout(LayoutKind.Sequential, Pack = 4)]
+ public unsafe struct NvapiUnicodeString
+ {
+ private fixed byte _data[4096];
+
+ public NvapiUnicodeString(string text)
+ {
+ Set(text);
+ }
+
+ public string Get()
+ {
+ fixed (byte* data = _data)
+ {
+ string text = Encoding.Unicode.GetString(data, 4096);
+
+ int index = text.IndexOf('\0');
+ if (index > -1)
+ {
+ text = text.Remove(index);
+ }
+
+ return text;
+ }
+ }
+
+ public void Set(string text)
+ {
+ text += '\0';
+ fixed (char* textPtr = text)
+ fixed (byte* data = _data)
+ {
+ int written = Encoding.Unicode.GetBytes(textPtr, text.Length, data, 4096);
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/GraphicsDriver/NVAPI/NvdrsApplicationV4.cs b/src/Ryujinx.Common/GraphicsDriver/NVAPI/NvdrsApplicationV4.cs
new file mode 100644
index 00000000..8b472cd1
--- /dev/null
+++ b/src/Ryujinx.Common/GraphicsDriver/NVAPI/NvdrsApplicationV4.cs
@@ -0,0 +1,17 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Common.GraphicsDriver.NVAPI
+{
+ [StructLayout(LayoutKind.Sequential, Pack = 4)]
+ unsafe struct NvdrsApplicationV4
+ {
+ public uint Version;
+ public uint IsPredefined;
+ public NvapiUnicodeString AppName;
+ public NvapiUnicodeString UserFriendlyName;
+ public NvapiUnicodeString Launcher;
+ public NvapiUnicodeString FileInFolder;
+ public uint Flags;
+ public NvapiUnicodeString CommandLine;
+ }
+}
diff --git a/src/Ryujinx.Common/GraphicsDriver/NVAPI/NvdrsProfile.cs b/src/Ryujinx.Common/GraphicsDriver/NVAPI/NvdrsProfile.cs
new file mode 100644
index 00000000..f1bfee82
--- /dev/null
+++ b/src/Ryujinx.Common/GraphicsDriver/NVAPI/NvdrsProfile.cs
@@ -0,0 +1,15 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Common.GraphicsDriver.NVAPI
+{
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ unsafe struct NvdrsProfile
+ {
+ public uint Version;
+ public NvapiUnicodeString ProfileName;
+ public uint GpuSupport;
+ public uint IsPredefined;
+ public uint NumOfApps;
+ public uint NumOfSettings;
+ }
+}
diff --git a/src/Ryujinx.Common/GraphicsDriver/NVAPI/NvdrsSetting.cs b/src/Ryujinx.Common/GraphicsDriver/NVAPI/NvdrsSetting.cs
new file mode 100644
index 00000000..ac188b35
--- /dev/null
+++ b/src/Ryujinx.Common/GraphicsDriver/NVAPI/NvdrsSetting.cs
@@ -0,0 +1,49 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Common.GraphicsDriver.NVAPI
+{
+ enum NvdrsSettingType : uint
+ {
+ NvdrsDwordType,
+ NvdrsBinaryType,
+ NvdrsStringType,
+ NvdrsWstringType,
+ }
+
+ enum NvdrsSettingLocation : uint
+ {
+ NvdrsCurrentProfileLocation,
+ NvdrsGlobalProfileLocation,
+ NvdrsBaseProfileLocation,
+ NvdrsDefaultProfileLocation,
+ }
+
+ [StructLayout(LayoutKind.Explicit, Size = 0x3020)]
+ unsafe struct NvdrsSetting
+ {
+ [FieldOffset(0x0)]
+ public uint Version;
+ [FieldOffset(0x4)]
+ public NvapiUnicodeString SettingName;
+ [FieldOffset(0x1004)]
+ public Nvapi SettingId;
+ [FieldOffset(0x1008)]
+ public NvdrsSettingType SettingType;
+ [FieldOffset(0x100C)]
+ public NvdrsSettingLocation SettingLocation;
+ [FieldOffset(0x1010)]
+ public uint IsCurrentPredefined;
+ [FieldOffset(0x1014)]
+ public uint IsPredefinedValid;
+
+ [FieldOffset(0x1018)]
+ public uint PredefinedValue;
+ [FieldOffset(0x1018)]
+ public NvapiUnicodeString PredefinedString;
+
+ [FieldOffset(0x201C)]
+ public uint CurrentValue;
+ [FieldOffset(0x201C)]
+ public NvapiUnicodeString CurrentString;
+ }
+}
diff --git a/src/Ryujinx.Common/GraphicsDriver/NVThreadedOptimization.cs b/src/Ryujinx.Common/GraphicsDriver/NVThreadedOptimization.cs
new file mode 100644
index 00000000..c5be6e37
--- /dev/null
+++ b/src/Ryujinx.Common/GraphicsDriver/NVThreadedOptimization.cs
@@ -0,0 +1,163 @@
+using Ryujinx.Common.GraphicsDriver.NVAPI;
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Common.GraphicsDriver
+{
+ static partial class NVThreadedOptimization
+ {
+ private const string ProfileName = "Ryujinx Nvidia Profile";
+
+ private const uint NvAPI_Initialize_ID = 0x0150E828;
+ private const uint NvAPI_DRS_CreateSession_ID = 0x0694D52E;
+ private const uint NvAPI_DRS_LoadSettings_ID = 0x375DBD6B;
+ private const uint NvAPI_DRS_FindProfileByName_ID = 0x7E4A9A0B;
+ private const uint NvAPI_DRS_CreateProfile_ID = 0x0CC176068;
+ private const uint NvAPI_DRS_CreateApplication_ID = 0x4347A9DE;
+ private const uint NvAPI_DRS_SetSetting_ID = 0x577DD202;
+ private const uint NvAPI_DRS_SaveSettings_ID = 0xFCBC7E14;
+ private const uint NvAPI_DRS_DestroySession_ID = 0x0DAD9CFF8;
+
+ [LibraryImport("nvapi64")]
+ private static partial IntPtr nvapi_QueryInterface(uint id);
+
+ private delegate int NvAPI_InitializeDelegate();
+ private static NvAPI_InitializeDelegate NvAPI_Initialize;
+
+ private delegate int NvAPI_DRS_CreateSessionDelegate(out IntPtr handle);
+ private static NvAPI_DRS_CreateSessionDelegate NvAPI_DRS_CreateSession;
+
+ private delegate int NvAPI_DRS_LoadSettingsDelegate(IntPtr handle);
+ private static NvAPI_DRS_LoadSettingsDelegate NvAPI_DRS_LoadSettings;
+
+ private delegate int NvAPI_DRS_FindProfileByNameDelegate(IntPtr handle, NvapiUnicodeString profileName, out IntPtr profileHandle);
+ private static NvAPI_DRS_FindProfileByNameDelegate NvAPI_DRS_FindProfileByName;
+
+ private delegate int NvAPI_DRS_CreateProfileDelegate(IntPtr handle, ref NvdrsProfile profileInfo, out IntPtr profileHandle);
+ private static NvAPI_DRS_CreateProfileDelegate NvAPI_DRS_CreateProfile;
+
+ private delegate int NvAPI_DRS_CreateApplicationDelegate(IntPtr handle, IntPtr profileHandle, ref NvdrsApplicationV4 app);
+ private static NvAPI_DRS_CreateApplicationDelegate NvAPI_DRS_CreateApplication;
+
+ private delegate int NvAPI_DRS_SetSettingDelegate(IntPtr handle, IntPtr profileHandle, ref NvdrsSetting setting);
+ private static NvAPI_DRS_SetSettingDelegate NvAPI_DRS_SetSetting;
+
+ private delegate int NvAPI_DRS_SaveSettingsDelegate(IntPtr handle);
+ private static NvAPI_DRS_SaveSettingsDelegate NvAPI_DRS_SaveSettings;
+
+ private delegate int NvAPI_DRS_DestroySessionDelegate(IntPtr handle);
+ private static NvAPI_DRS_DestroySessionDelegate NvAPI_DRS_DestroySession;
+
+ private static bool _initialized;
+
+ private static void Check(int status)
+ {
+ if (status != 0)
+ {
+ throw new Exception($"NVAPI Error: {status}");
+ }
+ }
+
+ private static void Initialize()
+ {
+ if (!_initialized)
+ {
+ NvAPI_Initialize = NvAPI_Delegate<NvAPI_InitializeDelegate>(NvAPI_Initialize_ID);
+
+ Check(NvAPI_Initialize());
+
+ NvAPI_DRS_CreateSession = NvAPI_Delegate<NvAPI_DRS_CreateSessionDelegate>(NvAPI_DRS_CreateSession_ID);
+ NvAPI_DRS_LoadSettings = NvAPI_Delegate<NvAPI_DRS_LoadSettingsDelegate>(NvAPI_DRS_LoadSettings_ID);
+ NvAPI_DRS_FindProfileByName = NvAPI_Delegate<NvAPI_DRS_FindProfileByNameDelegate>(NvAPI_DRS_FindProfileByName_ID);
+ NvAPI_DRS_CreateProfile = NvAPI_Delegate<NvAPI_DRS_CreateProfileDelegate>(NvAPI_DRS_CreateProfile_ID);
+ NvAPI_DRS_CreateApplication = NvAPI_Delegate<NvAPI_DRS_CreateApplicationDelegate>(NvAPI_DRS_CreateApplication_ID);
+ NvAPI_DRS_SetSetting = NvAPI_Delegate<NvAPI_DRS_SetSettingDelegate>(NvAPI_DRS_SetSetting_ID);
+ NvAPI_DRS_SaveSettings = NvAPI_Delegate<NvAPI_DRS_SaveSettingsDelegate>(NvAPI_DRS_SaveSettings_ID);
+ NvAPI_DRS_DestroySession = NvAPI_Delegate<NvAPI_DRS_DestroySessionDelegate>(NvAPI_DRS_DestroySession_ID);
+
+ _initialized = true;
+ }
+ }
+
+ private static uint MakeVersion<T>(uint version) where T : unmanaged
+ {
+ return (uint)Unsafe.SizeOf<T>() | version << 16;
+ }
+
+ public static void SetThreadedOptimization(bool enabled)
+ {
+ Initialize();
+
+ uint targetValue = (uint)(enabled ? Nvapi.OglThreadControlEnable : Nvapi.OglThreadControlDisable);
+
+ Check(NvAPI_Initialize());
+
+ Check(NvAPI_DRS_CreateSession(out IntPtr handle));
+
+ Check(NvAPI_DRS_LoadSettings(handle));
+
+ IntPtr profileHandle;
+
+ // Check if the profile already exists.
+
+ int status = NvAPI_DRS_FindProfileByName(handle, new NvapiUnicodeString(ProfileName), out profileHandle);
+
+ if (status != 0)
+ {
+ NvdrsProfile profile = new NvdrsProfile {
+ Version = MakeVersion<NvdrsProfile>(1),
+ IsPredefined = 0,
+ GpuSupport = uint.MaxValue
+ };
+ profile.ProfileName.Set(ProfileName);
+ Check(NvAPI_DRS_CreateProfile(handle, ref profile, out profileHandle));
+
+ NvdrsApplicationV4 application = new NvdrsApplicationV4
+ {
+ Version = MakeVersion<NvdrsApplicationV4>(4),
+ IsPredefined = 0,
+ Flags = 3 // IsMetro, IsCommandLine
+ };
+ application.AppName.Set("Ryujinx.exe");
+ application.UserFriendlyName.Set("Ryujinx");
+ application.Launcher.Set("");
+ application.FileInFolder.Set("");
+
+ Check(NvAPI_DRS_CreateApplication(handle, profileHandle, ref application));
+ }
+
+ NvdrsSetting setting = new NvdrsSetting
+ {
+ Version = MakeVersion<NvdrsSetting>(1),
+ SettingId = Nvapi.OglThreadControlId,
+ SettingType = NvdrsSettingType.NvdrsDwordType,
+ SettingLocation = NvdrsSettingLocation.NvdrsCurrentProfileLocation,
+ IsCurrentPredefined = 0,
+ IsPredefinedValid = 0,
+ CurrentValue = targetValue,
+ PredefinedValue = targetValue
+ };
+
+ Check(NvAPI_DRS_SetSetting(handle, profileHandle, ref setting));
+
+ Check(NvAPI_DRS_SaveSettings(handle));
+
+ NvAPI_DRS_DestroySession(handle);
+ }
+
+ private static T NvAPI_Delegate<T>(uint id) where T : class
+ {
+ IntPtr ptr = nvapi_QueryInterface(id);
+
+ if (ptr != IntPtr.Zero)
+ {
+ return Marshal.GetDelegateForFunctionPointer<T>(ptr);
+ }
+ else
+ {
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Hash128.cs b/src/Ryujinx.Common/Hash128.cs
new file mode 100644
index 00000000..04457bd0
--- /dev/null
+++ b/src/Ryujinx.Common/Hash128.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Common
+{
+ [StructLayout(LayoutKind.Sequential)]
+ public struct Hash128 : IEquatable<Hash128>
+ {
+ public ulong Low;
+ public ulong High;
+
+ public Hash128(ulong low, ulong high)
+ {
+ Low = low;
+ High = high;
+ }
+
+ public override string ToString()
+ {
+ return $"{High:x16}{Low:x16}";
+ }
+
+ public static bool operator ==(Hash128 x, Hash128 y)
+ {
+ return x.Equals(y);
+ }
+
+ public static bool operator !=(Hash128 x, Hash128 y)
+ {
+ return !x.Equals(y);
+ }
+
+ public override bool Equals(object obj)
+ {
+ return obj is Hash128 hash128 && Equals(hash128);
+ }
+
+ public bool Equals(Hash128 cmpObj)
+ {
+ return Low == cmpObj.Low && High == cmpObj.High;
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(Low, High);
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Logging/Formatters/DefaultLogFormatter.cs b/src/Ryujinx.Common/Logging/Formatters/DefaultLogFormatter.cs
new file mode 100644
index 00000000..28a7d546
--- /dev/null
+++ b/src/Ryujinx.Common/Logging/Formatters/DefaultLogFormatter.cs
@@ -0,0 +1,42 @@
+using System.Text;
+
+namespace Ryujinx.Common.Logging
+{
+ internal class DefaultLogFormatter : ILogFormatter
+ {
+ private static readonly ObjectPool<StringBuilder> StringBuilderPool = SharedPools.Default<StringBuilder>();
+
+ public string Format(LogEventArgs args)
+ {
+ StringBuilder sb = StringBuilderPool.Allocate();
+
+ try
+ {
+ sb.Clear();
+
+ sb.Append($@"{args.Time:hh\:mm\:ss\.fff}");
+ sb.Append($" |{args.Level.ToString()[0]}| ");
+
+ if (args.ThreadName != null)
+ {
+ sb.Append(args.ThreadName);
+ sb.Append(' ');
+ }
+
+ sb.Append(args.Message);
+
+ if (args.Data is not null)
+ {
+ sb.Append(' ');
+ DynamicObjectFormatter.Format(sb, args.Data);
+ }
+
+ return sb.ToString();
+ }
+ finally
+ {
+ StringBuilderPool.Release(sb);
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Logging/Formatters/DynamicObjectFormatter.cs b/src/Ryujinx.Common/Logging/Formatters/DynamicObjectFormatter.cs
new file mode 100644
index 00000000..5f15cc2a
--- /dev/null
+++ b/src/Ryujinx.Common/Logging/Formatters/DynamicObjectFormatter.cs
@@ -0,0 +1,84 @@
+#nullable enable
+using System;
+using System.Reflection;
+using System.Text;
+
+namespace Ryujinx.Common.Logging
+{
+ internal class DynamicObjectFormatter
+ {
+ private static readonly ObjectPool<StringBuilder> StringBuilderPool = SharedPools.Default<StringBuilder>();
+
+ public static string? Format(object? dynamicObject)
+ {
+ if (dynamicObject is null)
+ {
+ return null;
+ }
+
+ StringBuilder sb = StringBuilderPool.Allocate();
+
+ try
+ {
+ Format(sb, dynamicObject);
+
+ return sb.ToString();
+ }
+ finally
+ {
+ StringBuilderPool.Release(sb);
+ }
+ }
+
+ public static void Format(StringBuilder sb, object? dynamicObject)
+ {
+ if (dynamicObject is null)
+ {
+ return;
+ }
+
+ PropertyInfo[] props = dynamicObject.GetType().GetProperties();
+
+ sb.Append('{');
+
+ foreach (var prop in props)
+ {
+ sb.Append(prop.Name);
+ sb.Append(": ");
+
+ if (typeof(Array).IsAssignableFrom(prop.PropertyType))
+ {
+ Array? array = (Array?) prop.GetValue(dynamicObject);
+
+ if (array is not null)
+ {
+ foreach (var item in array)
+ {
+ sb.Append(item);
+ sb.Append(", ");
+ }
+
+ if (array.Length > 0)
+ {
+ sb.Remove(sb.Length - 2, 2);
+ }
+ }
+ }
+ else
+ {
+ sb.Append(prop.GetValue(dynamicObject));
+ }
+
+ sb.Append(" ; ");
+ }
+
+ // We remove the final ';' from the string
+ if (props.Length > 0)
+ {
+ sb.Remove(sb.Length - 3, 3);
+ }
+
+ sb.Append('}');
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Logging/Formatters/ILogFormatter.cs b/src/Ryujinx.Common/Logging/Formatters/ILogFormatter.cs
new file mode 100644
index 00000000..9a55bc6b
--- /dev/null
+++ b/src/Ryujinx.Common/Logging/Formatters/ILogFormatter.cs
@@ -0,0 +1,7 @@
+namespace Ryujinx.Common.Logging
+{
+ interface ILogFormatter
+ {
+ string Format(LogEventArgs args);
+ }
+}
diff --git a/src/Ryujinx.Common/Logging/LogClass.cs b/src/Ryujinx.Common/Logging/LogClass.cs
new file mode 100644
index 00000000..e62676cd
--- /dev/null
+++ b/src/Ryujinx.Common/Logging/LogClass.cs
@@ -0,0 +1,76 @@
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Logging
+{
+ [JsonConverter(typeof(TypedStringEnumConverter<LogClass>))]
+ public enum LogClass
+ {
+ Application,
+ Audio,
+ AudioRenderer,
+ Configuration,
+ Cpu,
+ Emulation,
+ FFmpeg,
+ Font,
+ Gpu,
+ Hid,
+ Host1x,
+ Kernel,
+ KernelIpc,
+ KernelScheduler,
+ KernelSvc,
+ Loader,
+ ModLoader,
+ Nvdec,
+ Ptc,
+ Service,
+ ServiceAcc,
+ ServiceAm,
+ ServiceApm,
+ ServiceAudio,
+ ServiceBcat,
+ ServiceBsd,
+ ServiceBtm,
+ ServiceCaps,
+ ServiceFatal,
+ ServiceFriend,
+ ServiceFs,
+ ServiceHid,
+ ServiceIrs,
+ ServiceLdn,
+ ServiceLdr,
+ ServiceLm,
+ ServiceMii,
+ ServiceMm,
+ ServiceMnpp,
+ ServiceNfc,
+ ServiceNfp,
+ ServiceNgct,
+ ServiceNifm,
+ ServiceNim,
+ ServiceNs,
+ ServiceNsd,
+ ServiceNtc,
+ ServiceNv,
+ ServiceOlsc,
+ ServicePctl,
+ ServicePcv,
+ ServicePl,
+ ServicePrepo,
+ ServicePsm,
+ ServicePtm,
+ ServiceSet,
+ ServiceSfdnsres,
+ ServiceSm,
+ ServiceSsl,
+ ServiceSss,
+ ServiceTime,
+ ServiceVi,
+ SurfaceFlinger,
+ TamperMachine,
+ Ui,
+ Vic
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Logging/LogEventArgs.cs b/src/Ryujinx.Common/Logging/LogEventArgs.cs
new file mode 100644
index 00000000..a27af780
--- /dev/null
+++ b/src/Ryujinx.Common/Logging/LogEventArgs.cs
@@ -0,0 +1,23 @@
+using System;
+
+namespace Ryujinx.Common.Logging
+{
+ public class LogEventArgs : EventArgs
+ {
+ public readonly LogLevel Level;
+ public readonly TimeSpan Time;
+ public readonly string ThreadName;
+
+ public readonly string Message;
+ public readonly object Data;
+
+ public LogEventArgs(LogLevel level, TimeSpan time, string threadName, string message, object data = null)
+ {
+ Level = level;
+ Time = time;
+ ThreadName = threadName;
+ Message = message;
+ Data = data;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Logging/LogEventArgsJson.cs b/src/Ryujinx.Common/Logging/LogEventArgsJson.cs
new file mode 100644
index 00000000..425b9766
--- /dev/null
+++ b/src/Ryujinx.Common/Logging/LogEventArgsJson.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Logging
+{
+ internal class LogEventArgsJson
+ {
+ public LogLevel Level { get; }
+ public TimeSpan Time { get; }
+ public string ThreadName { get; }
+
+ public string Message { get; }
+ public string Data { get; }
+
+ [JsonConstructor]
+ public LogEventArgsJson(LogLevel level, TimeSpan time, string threadName, string message, string data = null)
+ {
+ Level = level;
+ Time = time;
+ ThreadName = threadName;
+ Message = message;
+ Data = data;
+ }
+
+ public static LogEventArgsJson FromLogEventArgs(LogEventArgs args)
+ {
+ return new LogEventArgsJson(args.Level, args.Time, args.ThreadName, args.Message, DynamicObjectFormatter.Format(args.Data));
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Logging/LogEventJsonSerializerContext.cs b/src/Ryujinx.Common/Logging/LogEventJsonSerializerContext.cs
new file mode 100644
index 00000000..da21f11e
--- /dev/null
+++ b/src/Ryujinx.Common/Logging/LogEventJsonSerializerContext.cs
@@ -0,0 +1,9 @@
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Logging
+{
+ [JsonSerializable(typeof(LogEventArgsJson))]
+ internal partial class LogEventJsonSerializerContext : JsonSerializerContext
+ {
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Logging/LogLevel.cs b/src/Ryujinx.Common/Logging/LogLevel.cs
new file mode 100644
index 00000000..3786c756
--- /dev/null
+++ b/src/Ryujinx.Common/Logging/LogLevel.cs
@@ -0,0 +1,19 @@
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Logging
+{
+ [JsonConverter(typeof(TypedStringEnumConverter<LogLevel>))]
+ public enum LogLevel
+ {
+ Debug,
+ Stub,
+ Info,
+ Warning,
+ Error,
+ Guest,
+ AccessLog,
+ Notice,
+ Trace
+ }
+}
diff --git a/src/Ryujinx.Common/Logging/Logger.cs b/src/Ryujinx.Common/Logging/Logger.cs
new file mode 100644
index 00000000..4d48dd48
--- /dev/null
+++ b/src/Ryujinx.Common/Logging/Logger.cs
@@ -0,0 +1,224 @@
+using Ryujinx.Common.SystemInterop;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Threading;
+
+namespace Ryujinx.Common.Logging
+{
+ public static class Logger
+ {
+ private static readonly Stopwatch m_Time;
+
+ private static readonly bool[] m_EnabledClasses;
+
+ private static readonly List<ILogTarget> m_LogTargets;
+
+ private static readonly StdErrAdapter _stdErrAdapter;
+
+ public static event EventHandler<LogEventArgs> Updated;
+
+ public readonly struct Log
+ {
+ internal readonly LogLevel Level;
+
+ internal Log(LogLevel level)
+ {
+ Level = level;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void PrintMsg(LogClass logClass, string message)
+ {
+ if (m_EnabledClasses[(int)logClass])
+ {
+ Updated?.Invoke(null, new LogEventArgs(Level, m_Time.Elapsed, Thread.CurrentThread.Name, FormatMessage(logClass, "", message)));
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Print(LogClass logClass, string message, [CallerMemberName] string caller = "")
+ {
+ if (m_EnabledClasses[(int)logClass])
+ {
+ Updated?.Invoke(null, new LogEventArgs(Level, m_Time.Elapsed, Thread.CurrentThread.Name, FormatMessage(logClass, caller, message)));
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Print(LogClass logClass, string message, object data, [CallerMemberName] string caller = "")
+ {
+ if (m_EnabledClasses[(int)logClass])
+ {
+ Updated?.Invoke(null, new LogEventArgs(Level, m_Time.Elapsed, Thread.CurrentThread.Name, FormatMessage(logClass, caller, message), data));
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void PrintStub(LogClass logClass, string message = "", [CallerMemberName] string caller = "")
+ {
+ if (m_EnabledClasses[(int)logClass])
+ {
+ Updated?.Invoke(null, new LogEventArgs(Level, m_Time.Elapsed, Thread.CurrentThread.Name, FormatMessage(logClass, caller, "Stubbed. " + message)));
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void PrintStub(LogClass logClass, object data, [CallerMemberName] string caller = "")
+ {
+ if (m_EnabledClasses[(int)logClass])
+ {
+ Updated?.Invoke(null, new LogEventArgs(Level, m_Time.Elapsed, Thread.CurrentThread.Name, FormatMessage(logClass, caller, "Stubbed."), data));
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void PrintStub(LogClass logClass, string message, object data, [CallerMemberName] string caller = "")
+ {
+ if (m_EnabledClasses[(int)logClass])
+ {
+ Updated?.Invoke(null, new LogEventArgs(Level, m_Time.Elapsed, Thread.CurrentThread.Name, FormatMessage(logClass, caller, "Stubbed. " + message), data));
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void PrintRawMsg(string message)
+ {
+ Updated?.Invoke(null, new LogEventArgs(Level, m_Time.Elapsed, Thread.CurrentThread.Name, message));
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static string FormatMessage(LogClass Class, string Caller, string Message) => $"{Class} {Caller}: {Message}";
+ }
+
+ public static Log? Debug { get; private set; }
+ public static Log? Info { get; private set; }
+ public static Log? Warning { get; private set; }
+ public static Log? Error { get; private set; }
+ public static Log? Guest { get; private set; }
+ public static Log? AccessLog { get; private set; }
+ public static Log? Stub { get; private set; }
+ public static Log? Trace { get; private set; }
+ public static Log Notice { get; } // Always enabled
+
+ static Logger()
+ {
+ m_EnabledClasses = new bool[Enum.GetNames<LogClass>().Length];
+
+ for (int index = 0; index < m_EnabledClasses.Length; index++)
+ {
+ m_EnabledClasses[index] = true;
+ }
+
+ m_LogTargets = new List<ILogTarget>();
+
+ m_Time = Stopwatch.StartNew();
+
+ // Logger should log to console by default
+ AddTarget(new AsyncLogTargetWrapper(
+ new ConsoleLogTarget("console"),
+ 1000,
+ AsyncLogTargetOverflowAction.Discard));
+
+ Notice = new Log(LogLevel.Notice);
+
+ // Enable important log levels before configuration is loaded
+ Error = new Log(LogLevel.Error);
+ Warning = new Log(LogLevel.Warning);
+ Info = new Log(LogLevel.Info);
+ Trace = new Log(LogLevel.Trace);
+
+ _stdErrAdapter = new StdErrAdapter();
+ }
+
+ public static void RestartTime()
+ {
+ m_Time.Restart();
+ }
+
+ private static ILogTarget GetTarget(string targetName)
+ {
+ foreach (var target in m_LogTargets)
+ {
+ if (target.Name.Equals(targetName))
+ {
+ return target;
+ }
+ }
+
+ return null;
+ }
+
+ public static void AddTarget(ILogTarget target)
+ {
+ m_LogTargets.Add(target);
+
+ Updated += target.Log;
+ }
+
+ public static void RemoveTarget(string target)
+ {
+ ILogTarget logTarget = GetTarget(target);
+
+ if (logTarget != null)
+ {
+ Updated -= logTarget.Log;
+
+ m_LogTargets.Remove(logTarget);
+
+ logTarget.Dispose();
+ }
+ }
+
+ public static void Shutdown()
+ {
+ Updated = null;
+
+ _stdErrAdapter.Dispose();
+
+ foreach (var target in m_LogTargets)
+ {
+ target.Dispose();
+ }
+
+ m_LogTargets.Clear();
+ }
+
+ public static IReadOnlyCollection<LogLevel> GetEnabledLevels()
+ {
+ var logs = new Log?[] { Debug, Info, Warning, Error, Guest, AccessLog, Stub, Trace };
+ List<LogLevel> levels = new List<LogLevel>(logs.Length);
+ foreach (var log in logs)
+ {
+ if (log.HasValue)
+ {
+ levels.Add(log.Value.Level);
+ }
+ }
+
+ return levels;
+ }
+
+ public static void SetEnable(LogLevel logLevel, bool enabled)
+ {
+ switch (logLevel)
+ {
+ case LogLevel.Debug : Debug = enabled ? new Log(LogLevel.Debug) : new Log?(); break;
+ case LogLevel.Info : Info = enabled ? new Log(LogLevel.Info) : new Log?(); break;
+ case LogLevel.Warning : Warning = enabled ? new Log(LogLevel.Warning) : new Log?(); break;
+ case LogLevel.Error : Error = enabled ? new Log(LogLevel.Error) : new Log?(); break;
+ case LogLevel.Guest : Guest = enabled ? new Log(LogLevel.Guest) : new Log?(); break;
+ case LogLevel.AccessLog : AccessLog = enabled ? new Log(LogLevel.AccessLog): new Log?(); break;
+ case LogLevel.Stub : Stub = enabled ? new Log(LogLevel.Stub) : new Log?(); break;
+ case LogLevel.Trace : Trace = enabled ? new Log(LogLevel.Trace) : new Log?(); break;
+ default: throw new ArgumentException("Unknown Log Level");
+ }
+ }
+
+ public static void SetEnable(LogClass logClass, bool enabled)
+ {
+ m_EnabledClasses[(int)logClass] = enabled;
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs b/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs
new file mode 100644
index 00000000..43c62d31
--- /dev/null
+++ b/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Concurrent;
+using System.Threading;
+
+namespace Ryujinx.Common.Logging
+{
+ public enum AsyncLogTargetOverflowAction
+ {
+ /// <summary>
+ /// Block until there's more room in the queue
+ /// </summary>
+ Block = 0,
+
+ /// <summary>
+ /// Discard the overflowing item
+ /// </summary>
+ Discard = 1
+ }
+
+ public class AsyncLogTargetWrapper : ILogTarget
+ {
+ private ILogTarget _target;
+
+ private Thread _messageThread;
+
+ private BlockingCollection<LogEventArgs> _messageQueue;
+
+ private readonly int _overflowTimeout;
+
+ string ILogTarget.Name { get => _target.Name; }
+
+ public AsyncLogTargetWrapper(ILogTarget target)
+ : this(target, -1, AsyncLogTargetOverflowAction.Block)
+ { }
+
+ public AsyncLogTargetWrapper(ILogTarget target, int queueLimit, AsyncLogTargetOverflowAction overflowAction)
+ {
+ _target = target;
+ _messageQueue = new BlockingCollection<LogEventArgs>(queueLimit);
+ _overflowTimeout = overflowAction == AsyncLogTargetOverflowAction.Block ? -1 : 0;
+
+ _messageThread = new Thread(() => {
+ while (!_messageQueue.IsCompleted)
+ {
+ try
+ {
+ _target.Log(this, _messageQueue.Take());
+ }
+ catch (InvalidOperationException)
+ {
+ // IOE means that Take() was called on a completed collection.
+ // Some other thread can call CompleteAdding after we pass the
+ // IsCompleted check but before we call Take.
+ // We can simply catch the exception since the loop will break
+ // on the next iteration.
+ }
+ }
+ });
+
+ _messageThread.Name = "Logger.MessageThread";
+ _messageThread.IsBackground = true;
+ _messageThread.Start();
+ }
+
+ public void Log(object sender, LogEventArgs e)
+ {
+ if (!_messageQueue.IsAddingCompleted)
+ {
+ _messageQueue.TryAdd(e, _overflowTimeout);
+ }
+ }
+
+ public void Dispose()
+ {
+ _messageQueue.CompleteAdding();
+ _messageThread.Join();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Logging/Targets/ConsoleLogTarget.cs b/src/Ryujinx.Common/Logging/Targets/ConsoleLogTarget.cs
new file mode 100644
index 00000000..7b77c4f2
--- /dev/null
+++ b/src/Ryujinx.Common/Logging/Targets/ConsoleLogTarget.cs
@@ -0,0 +1,41 @@
+using System;
+
+namespace Ryujinx.Common.Logging
+{
+ public class ConsoleLogTarget : ILogTarget
+ {
+ private readonly ILogFormatter _formatter;
+
+ private readonly string _name;
+
+ string ILogTarget.Name { get => _name; }
+
+ private static ConsoleColor GetLogColor(LogLevel level) => level switch {
+ LogLevel.Info => ConsoleColor.White,
+ LogLevel.Warning => ConsoleColor.Yellow,
+ LogLevel.Error => ConsoleColor.Red,
+ LogLevel.Stub => ConsoleColor.DarkGray,
+ LogLevel.Notice => ConsoleColor.Cyan,
+ LogLevel.Trace => ConsoleColor.DarkCyan,
+ _ => ConsoleColor.Gray,
+ };
+
+ public ConsoleLogTarget(string name)
+ {
+ _formatter = new DefaultLogFormatter();
+ _name = name;
+ }
+
+ public void Log(object sender, LogEventArgs args)
+ {
+ Console.ForegroundColor = GetLogColor(args.Level);
+ Console.WriteLine(_formatter.Format(args));
+ Console.ResetColor();
+ }
+
+ public void Dispose()
+ {
+ Console.ResetColor();
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs b/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs
new file mode 100644
index 00000000..24dd6d17
--- /dev/null
+++ b/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs
@@ -0,0 +1,55 @@
+using System;
+using System.IO;
+using System.Linq;
+
+namespace Ryujinx.Common.Logging
+{
+ public class FileLogTarget : ILogTarget
+ {
+ private readonly StreamWriter _logWriter;
+ private readonly ILogFormatter _formatter;
+ private readonly string _name;
+
+ string ILogTarget.Name { get => _name; }
+
+ public FileLogTarget(string path, string name)
+ : this(path, name, FileShare.Read, FileMode.Append)
+ { }
+
+ public FileLogTarget(string path, string name, FileShare fileShare, FileMode fileMode)
+ {
+ // Ensure directory is present
+ DirectoryInfo logDir = new DirectoryInfo(Path.Combine(path, "Logs"));
+ logDir.Create();
+
+ // Clean up old logs, should only keep 3
+ FileInfo[] files = logDir.GetFiles("*.log").OrderBy((info => info.CreationTime)).ToArray();
+ for (int i = 0; i < files.Length - 2; i++)
+ {
+ files[i].Delete();
+ }
+
+ string version = ReleaseInformation.GetVersion();
+
+ // Get path for the current time
+ path = Path.Combine(logDir.FullName, $"Ryujinx_{version}_{DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")}.log");
+
+ _name = name;
+ _logWriter = new StreamWriter(File.Open(path, fileMode, FileAccess.Write, fileShare));
+ _formatter = new DefaultLogFormatter();
+ }
+
+ public void Log(object sender, LogEventArgs args)
+ {
+ _logWriter.WriteLine(_formatter.Format(args));
+ _logWriter.Flush();
+ }
+
+ public void Dispose()
+ {
+ _logWriter.WriteLine("---- End of Log ----");
+ _logWriter.Flush();
+ _logWriter.Dispose();
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Logging/Targets/ILogTarget.cs b/src/Ryujinx.Common/Logging/Targets/ILogTarget.cs
new file mode 100644
index 00000000..d4d26a93
--- /dev/null
+++ b/src/Ryujinx.Common/Logging/Targets/ILogTarget.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace Ryujinx.Common.Logging
+{
+ public interface ILogTarget : IDisposable
+ {
+ void Log(object sender, LogEventArgs args);
+
+ string Name { get; }
+ }
+}
diff --git a/src/Ryujinx.Common/Logging/Targets/JsonLogTarget.cs b/src/Ryujinx.Common/Logging/Targets/JsonLogTarget.cs
new file mode 100644
index 00000000..06976433
--- /dev/null
+++ b/src/Ryujinx.Common/Logging/Targets/JsonLogTarget.cs
@@ -0,0 +1,40 @@
+using Ryujinx.Common.Utilities;
+using System.IO;
+
+namespace Ryujinx.Common.Logging
+{
+ public class JsonLogTarget : ILogTarget
+ {
+ private Stream _stream;
+ private bool _leaveOpen;
+ private string _name;
+
+ string ILogTarget.Name { get => _name; }
+
+ public JsonLogTarget(Stream stream, string name)
+ {
+ _stream = stream;
+ _name = name;
+ }
+
+ public JsonLogTarget(Stream stream, bool leaveOpen)
+ {
+ _stream = stream;
+ _leaveOpen = leaveOpen;
+ }
+
+ public void Log(object sender, LogEventArgs e)
+ {
+ var logEventArgsJson = LogEventArgsJson.FromLogEventArgs(e);
+ JsonHelper.SerializeToStream(_stream, logEventArgsJson, LogEventJsonSerializerContext.Default.LogEventArgsJson);
+ }
+
+ public void Dispose()
+ {
+ if (!_leaveOpen)
+ {
+ _stream.Dispose();
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Memory/ArrayPtr.cs b/src/Ryujinx.Common/Memory/ArrayPtr.cs
new file mode 100644
index 00000000..9e95f75e
--- /dev/null
+++ b/src/Ryujinx.Common/Memory/ArrayPtr.cs
@@ -0,0 +1,123 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Common.Memory
+{
+ /// <summary>
+ /// Represents an array of unmanaged resources.
+ /// </summary>
+ /// <typeparam name="T">Array element type</typeparam>
+ public unsafe struct ArrayPtr<T> : IEquatable<ArrayPtr<T>>, IArray<T> where T : unmanaged
+ {
+ private IntPtr _ptr;
+
+ /// <summary>
+ /// Null pointer.
+ /// </summary>
+ public static ArrayPtr<T> Null => new ArrayPtr<T>() { _ptr = IntPtr.Zero };
+
+ /// <summary>
+ /// True if the pointer is null, false otherwise.
+ /// </summary>
+ public bool IsNull => _ptr == IntPtr.Zero;
+
+ /// <summary>
+ /// Number of elements on the array.
+ /// </summary>
+ public int Length { get; }
+
+ /// <summary>
+ /// Gets a reference to the item at the given index.
+ /// </summary>
+ /// <remarks>
+ /// No bounds checks are performed, this allows negative indexing,
+ /// but care must be taken if the index may be out of bounds.
+ /// </remarks>
+ /// <param name="index">Index of the element</param>
+ /// <returns>Reference to the element at the given index</returns>
+ public ref T this[int index] => ref Unsafe.AsRef<T>((T*)_ptr + index);
+
+ /// <summary>
+ /// Creates a new array from a given reference.
+ /// </summary>
+ /// <remarks>
+ /// For data on the heap, proper pinning is necessary during
+ /// use. Failure to do so will result in memory corruption and crashes.
+ /// </remarks>
+ /// <param name="value">Reference of the first array element</param>
+ /// <param name="length">Number of elements on the array</param>
+ public ArrayPtr(ref T value, int length)
+ {
+ _ptr = (IntPtr)Unsafe.AsPointer(ref value);
+ Length = length;
+ }
+
+ /// <summary>
+ /// Creates a new array from a given pointer.
+ /// </summary>
+ /// <param name="ptr">Array base pointer</param>
+ /// <param name="length">Number of elements on the array</param>
+ public ArrayPtr(T* ptr, int length)
+ {
+ _ptr = (IntPtr)ptr;
+ Length = length;
+ }
+
+ /// <summary>
+ /// Creates a new array from a given pointer.
+ /// </summary>
+ /// <param name="ptr">Array base pointer</param>
+ /// <param name="length">Number of elements on the array</param>
+ public ArrayPtr(IntPtr ptr, int length)
+ {
+ _ptr = ptr;
+ Length = length;
+ }
+
+ /// <summary>
+ /// Splits the array starting at the specified position.
+ /// </summary>
+ /// <param name="start">Index where the new array should start</param>
+ /// <returns>New array starting at the specified position</returns>
+ public ArrayPtr<T> Slice(int start) => new ArrayPtr<T>(ref this[start], Length - start);
+
+ /// <summary>
+ /// Gets a span from the array.
+ /// </summary>
+ /// <returns>Span of the array</returns>
+ public Span<T> AsSpan() => Length == 0 ? Span<T>.Empty : MemoryMarshal.CreateSpan(ref this[0], Length);
+
+ /// <summary>
+ /// Gets the array base pointer.
+ /// </summary>
+ /// <returns>Base pointer</returns>
+ public T* ToPointer() => (T*)_ptr;
+
+ public override bool Equals(object obj)
+ {
+ return obj is ArrayPtr<T> other && Equals(other);
+ }
+
+ public bool Equals([AllowNull] ArrayPtr<T> other)
+ {
+ return _ptr == other._ptr && Length == other.Length;
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(_ptr, Length);
+ }
+
+ public static bool operator ==(ArrayPtr<T> left, ArrayPtr<T> right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(ArrayPtr<T> left, ArrayPtr<T> right)
+ {
+ return !(left == right);
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Memory/Box.cs b/src/Ryujinx.Common/Memory/Box.cs
new file mode 100644
index 00000000..743cc31d
--- /dev/null
+++ b/src/Ryujinx.Common/Memory/Box.cs
@@ -0,0 +1,12 @@
+namespace Ryujinx.Common.Memory
+{
+ public class Box<T> where T : unmanaged
+ {
+ public T Data;
+
+ public Box()
+ {
+ Data = new T();
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Memory/ByteMemoryPool.ByteMemoryPoolBuffer.cs b/src/Ryujinx.Common/Memory/ByteMemoryPool.ByteMemoryPoolBuffer.cs
new file mode 100644
index 00000000..eda350bd
--- /dev/null
+++ b/src/Ryujinx.Common/Memory/ByteMemoryPool.ByteMemoryPoolBuffer.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Buffers;
+using System.Threading;
+
+namespace Ryujinx.Common.Memory
+{
+ public sealed partial class ByteMemoryPool
+ {
+ /// <summary>
+ /// Represents a <see cref="IMemoryOwner{Byte}"/> that wraps an array rented from
+ /// <see cref="ArrayPool{Byte}.Shared"/> and exposes it as <see cref="Memory{Byte}"/>
+ /// with a length of the requested size.
+ /// </summary>
+ private sealed class ByteMemoryPoolBuffer : IMemoryOwner<byte>
+ {
+ private byte[] _array;
+ private readonly int _length;
+
+ public ByteMemoryPoolBuffer(int length)
+ {
+ _array = ArrayPool<byte>.Shared.Rent(length);
+ _length = length;
+ }
+
+ /// <summary>
+ /// Returns a <see cref="Memory{Byte}"/> belonging to this owner.
+ /// </summary>
+ public Memory<byte> Memory
+ {
+ get
+ {
+ byte[] array = _array;
+
+ ObjectDisposedException.ThrowIf(array is null, this);
+
+ return new Memory<byte>(array, 0, _length);
+ }
+ }
+
+ public void Dispose()
+ {
+ var array = Interlocked.Exchange(ref _array, null);
+
+ if (array != null)
+ {
+ ArrayPool<byte>.Shared.Return(array);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Memory/ByteMemoryPool.cs b/src/Ryujinx.Common/Memory/ByteMemoryPool.cs
new file mode 100644
index 00000000..2910f408
--- /dev/null
+++ b/src/Ryujinx.Common/Memory/ByteMemoryPool.cs
@@ -0,0 +1,108 @@
+using System;
+using System.Buffers;
+
+namespace Ryujinx.Common.Memory
+{
+ /// <summary>
+ /// Provides a pool of re-usable byte array instances.
+ /// </summary>
+ public sealed partial class ByteMemoryPool
+ {
+ private static readonly ByteMemoryPool _shared = new ByteMemoryPool();
+
+ /// <summary>
+ /// Constructs a <see cref="ByteMemoryPool"/> instance. Private to force access through
+ /// the <see cref="ByteMemoryPool.Shared"/> instance.
+ /// </summary>
+ private ByteMemoryPool()
+ {
+ // No implementation
+ }
+
+ /// <summary>
+ /// Retrieves a shared <see cref="ByteMemoryPool"/> instance.
+ /// </summary>
+ public static ByteMemoryPool Shared => _shared;
+
+ /// <summary>
+ /// Returns the maximum buffer size supported by this pool.
+ /// </summary>
+ public int MaxBufferSize => Array.MaxLength;
+
+ /// <summary>
+ /// Rents a byte memory buffer from <see cref="ArrayPool{Byte}.Shared"/>.
+ /// The buffer may contain data from a prior use.
+ /// </summary>
+ /// <param name="length">The buffer's required length in bytes</param>
+ /// <returns>A <see cref="IMemoryOwner{Byte}"/> wrapping the rented memory</returns>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public IMemoryOwner<byte> Rent(long length)
+ => RentImpl(checked((int)length));
+
+ /// <summary>
+ /// Rents a byte memory buffer from <see cref="ArrayPool{Byte}.Shared"/>.
+ /// The buffer may contain data from a prior use.
+ /// </summary>
+ /// <param name="length">The buffer's required length in bytes</param>
+ /// <returns>A <see cref="IMemoryOwner{Byte}"/> wrapping the rented memory</returns>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public IMemoryOwner<byte> Rent(ulong length)
+ => RentImpl(checked((int)length));
+
+ /// <summary>
+ /// Rents a byte memory buffer from <see cref="ArrayPool{Byte}.Shared"/>.
+ /// The buffer may contain data from a prior use.
+ /// </summary>
+ /// <param name="length">The buffer's required length in bytes</param>
+ /// <returns>A <see cref="IMemoryOwner{Byte}"/> wrapping the rented memory</returns>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public IMemoryOwner<byte> Rent(int length)
+ => RentImpl(length);
+
+ /// <summary>
+ /// Rents a byte memory buffer from <see cref="ArrayPool{Byte}.Shared"/>.
+ /// The buffer's contents are cleared (set to all 0s) before returning.
+ /// </summary>
+ /// <param name="length">The buffer's required length in bytes</param>
+ /// <returns>A <see cref="IMemoryOwner{Byte}"/> wrapping the rented memory</returns>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public IMemoryOwner<byte> RentCleared(long length)
+ => RentCleared(checked((int)length));
+
+ /// <summary>
+ /// Rents a byte memory buffer from <see cref="ArrayPool{Byte}.Shared"/>.
+ /// The buffer's contents are cleared (set to all 0s) before returning.
+ /// </summary>
+ /// <param name="length">The buffer's required length in bytes</param>
+ /// <returns>A <see cref="IMemoryOwner{Byte}"/> wrapping the rented memory</returns>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public IMemoryOwner<byte> RentCleared(ulong length)
+ => RentCleared(checked((int)length));
+
+ /// <summary>
+ /// Rents a byte memory buffer from <see cref="ArrayPool{Byte}.Shared"/>.
+ /// The buffer's contents are cleared (set to all 0s) before returning.
+ /// </summary>
+ /// <param name="length">The buffer's required length in bytes</param>
+ /// <returns>A <see cref="IMemoryOwner{Byte}"/> wrapping the rented memory</returns>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public IMemoryOwner<byte> RentCleared(int length)
+ {
+ var buffer = RentImpl(length);
+
+ buffer.Memory.Span.Clear();
+
+ return buffer;
+ }
+
+ private static ByteMemoryPoolBuffer RentImpl(int length)
+ {
+ if ((uint)length > Array.MaxLength)
+ {
+ throw new ArgumentOutOfRangeException(nameof(length), length, null);
+ }
+
+ return new ByteMemoryPoolBuffer(length);
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Memory/IArray.cs b/src/Ryujinx.Common/Memory/IArray.cs
new file mode 100644
index 00000000..8f17fade
--- /dev/null
+++ b/src/Ryujinx.Common/Memory/IArray.cs
@@ -0,0 +1,21 @@
+namespace Ryujinx.Common.Memory
+{
+ /// <summary>
+ /// Array interface.
+ /// </summary>
+ /// <typeparam name="T">Element type</typeparam>
+ public interface IArray<T> where T : unmanaged
+ {
+ /// <summary>
+ /// Used to index the array.
+ /// </summary>
+ /// <param name="index">Element index</param>
+ /// <returns>Element at the specified index</returns>
+ ref T this[int index] { get; }
+
+ /// <summary>
+ /// Number of elements on the array.
+ /// </summary>
+ int Length { get; }
+ }
+}
diff --git a/src/Ryujinx.Common/Memory/MemoryStreamManager.cs b/src/Ryujinx.Common/Memory/MemoryStreamManager.cs
new file mode 100644
index 00000000..68b82999
--- /dev/null
+++ b/src/Ryujinx.Common/Memory/MemoryStreamManager.cs
@@ -0,0 +1,99 @@
+using Microsoft.IO;
+using System;
+
+namespace Ryujinx.Common.Memory
+{
+ public static class MemoryStreamManager
+ {
+ private static readonly RecyclableMemoryStreamManager _shared = new RecyclableMemoryStreamManager();
+
+ /// <summary>
+ /// We don't expose the <c>RecyclableMemoryStreamManager</c> directly because version 2.x
+ /// returns them as <c>MemoryStream</c>. This Shared class is here to a) offer only the GetStream() versions we use
+ /// and b) return them as <c>RecyclableMemoryStream</c> so we don't have to cast.
+ /// </summary>
+ public static class Shared
+ {
+ /// <summary>
+ /// Retrieve a new <c>MemoryStream</c> object with no tag and a default initial capacity.
+ /// </summary>
+ /// <returns>A <c>RecyclableMemoryStream</c></returns>
+ public static RecyclableMemoryStream GetStream()
+ => new RecyclableMemoryStream(_shared);
+
+ /// <summary>
+ /// Retrieve a new <c>MemoryStream</c> object with the contents copied from the provided
+ /// buffer. The provided buffer is not wrapped or used after construction.
+ /// </summary>
+ /// <remarks>The new stream's position is set to the beginning of the stream when returned.</remarks>
+ /// <param name="buffer">The byte buffer to copy data from</param>
+ /// <returns>A <c>RecyclableMemoryStream</c></returns>
+ public static RecyclableMemoryStream GetStream(byte[] buffer)
+ => GetStream(Guid.NewGuid(), null, buffer, 0, buffer.Length);
+
+ /// <summary>
+ /// Retrieve a new <c>MemoryStream</c> object with the given tag and with contents copied from the provided
+ /// buffer. The provided buffer is not wrapped or used after construction.
+ /// </summary>
+ /// <remarks>The new stream's position is set to the beginning of the stream when returned.</remarks>
+ /// <param name="buffer">The byte buffer to copy data from</param>
+ /// <returns>A <c>RecyclableMemoryStream</c></returns>
+ public static RecyclableMemoryStream GetStream(ReadOnlySpan<byte> buffer)
+ => GetStream(Guid.NewGuid(), null, buffer);
+
+ /// <summary>
+ /// Retrieve a new <c>RecyclableMemoryStream</c> object with the given tag and with contents copied from the provided
+ /// buffer. The provided buffer is not wrapped or used after construction.
+ /// </summary>
+ /// <remarks>The new stream's position is set to the beginning of the stream when returned.</remarks>
+ /// <param name="id">A unique identifier which can be used to trace usages of the stream</param>
+ /// <param name="tag">A tag which can be used to track the source of the stream</param>
+ /// <param name="buffer">The byte buffer to copy data from</param>
+ /// <returns>A <c>RecyclableMemoryStream</c></returns>
+ public static RecyclableMemoryStream GetStream(Guid id, string tag, ReadOnlySpan<byte> buffer)
+ {
+ RecyclableMemoryStream stream = null;
+ try
+ {
+ stream = new RecyclableMemoryStream(_shared, id, tag, buffer.Length);
+ stream.Write(buffer);
+ stream.Position = 0;
+ return stream;
+ }
+ catch
+ {
+ stream?.Dispose();
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// Retrieve a new <c>RecyclableMemoryStream</c> object with the given tag and with contents copied from the provided
+ /// buffer. The provided buffer is not wrapped or used after construction.
+ /// </summary>
+ /// <remarks>The new stream's position is set to the beginning of the stream when returned</remarks>
+ /// <param name="id">A unique identifier which can be used to trace usages of the stream</param>
+ /// <param name="tag">A tag which can be used to track the source of the stream</param>
+ /// <param name="buffer">The byte buffer to copy data from</param>
+ /// <param name="offset">The offset from the start of the buffer to copy from</param>
+ /// <param name="count">The number of bytes to copy from the buffer</param>
+ /// <returns>A <c>RecyclableMemoryStream</c></returns>
+ public static RecyclableMemoryStream GetStream(Guid id, string tag, byte[] buffer, int offset, int count)
+ {
+ RecyclableMemoryStream stream = null;
+ try
+ {
+ stream = new RecyclableMemoryStream(_shared, id, tag, count);
+ stream.Write(buffer, offset, count);
+ stream.Position = 0;
+ return stream;
+ }
+ catch
+ {
+ stream?.Dispose();
+ throw;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Memory/PartialUnmaps/NativeReaderWriterLock.cs b/src/Ryujinx.Common/Memory/PartialUnmaps/NativeReaderWriterLock.cs
new file mode 100644
index 00000000..78eeb16f
--- /dev/null
+++ b/src/Ryujinx.Common/Memory/PartialUnmaps/NativeReaderWriterLock.cs
@@ -0,0 +1,80 @@
+using System.Runtime.InteropServices;
+using System.Threading;
+
+using static Ryujinx.Common.Memory.PartialUnmaps.PartialUnmapHelpers;
+
+namespace Ryujinx.Common.Memory.PartialUnmaps
+{
+ /// <summary>
+ /// A simple implementation of a ReaderWriterLock which can be used from native code.
+ /// </summary>
+ [StructLayout(LayoutKind.Sequential, Pack = 4)]
+ public struct NativeReaderWriterLock
+ {
+ public int WriteLock;
+ public int ReaderCount;
+
+ public static int WriteLockOffset;
+ public static int ReaderCountOffset;
+
+ /// <summary>
+ /// Populates the field offsets for use when emitting native code.
+ /// </summary>
+ static NativeReaderWriterLock()
+ {
+ NativeReaderWriterLock instance = new NativeReaderWriterLock();
+
+ WriteLockOffset = OffsetOf(ref instance, ref instance.WriteLock);
+ ReaderCountOffset = OffsetOf(ref instance, ref instance.ReaderCount);
+ }
+
+ /// <summary>
+ /// Acquires the reader lock.
+ /// </summary>
+ public void AcquireReaderLock()
+ {
+ // Must take write lock for a very short time to become a reader.
+
+ while (Interlocked.CompareExchange(ref WriteLock, 1, 0) != 0) { }
+
+ Interlocked.Increment(ref ReaderCount);
+
+ Interlocked.Exchange(ref WriteLock, 0);
+ }
+
+ /// <summary>
+ /// Releases the reader lock.
+ /// </summary>
+ public void ReleaseReaderLock()
+ {
+ Interlocked.Decrement(ref ReaderCount);
+ }
+
+ /// <summary>
+ /// Upgrades to a writer lock. The reader lock is temporarily released while obtaining the writer lock.
+ /// </summary>
+ public void UpgradeToWriterLock()
+ {
+ // Prevent any more threads from entering reader.
+ // If the write lock is already taken, wait for it to not be taken.
+
+ Interlocked.Decrement(ref ReaderCount);
+
+ while (Interlocked.CompareExchange(ref WriteLock, 1, 0) != 0) { }
+
+ // Wait for reader count to drop to 0, then take the lock again as the only reader.
+
+ while (Interlocked.CompareExchange(ref ReaderCount, 1, 0) != 0) { }
+ }
+
+ /// <summary>
+ /// Downgrades from a writer lock, back to a reader one.
+ /// </summary>
+ public void DowngradeFromWriterLock()
+ {
+ // Release the WriteLock.
+
+ Interlocked.Exchange(ref WriteLock, 0);
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Memory/PartialUnmaps/PartialUnmapHelpers.cs b/src/Ryujinx.Common/Memory/PartialUnmaps/PartialUnmapHelpers.cs
new file mode 100644
index 00000000..e650de06
--- /dev/null
+++ b/src/Ryujinx.Common/Memory/PartialUnmaps/PartialUnmapHelpers.cs
@@ -0,0 +1,20 @@
+using System.Runtime.CompilerServices;
+
+namespace Ryujinx.Common.Memory.PartialUnmaps
+{
+ static class PartialUnmapHelpers
+ {
+ /// <summary>
+ /// Calculates a byte offset of a given field within a struct.
+ /// </summary>
+ /// <typeparam name="T">Struct type</typeparam>
+ /// <typeparam name="T2">Field type</typeparam>
+ /// <param name="storage">Parent struct</param>
+ /// <param name="target">Field</param>
+ /// <returns>The byte offset of the given field in the given struct</returns>
+ public static int OffsetOf<T, T2>(ref T2 storage, ref T target)
+ {
+ return (int)Unsafe.ByteOffset(ref Unsafe.As<T2, T>(ref storage), ref target);
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Memory/PartialUnmaps/PartialUnmapState.cs b/src/Ryujinx.Common/Memory/PartialUnmaps/PartialUnmapState.cs
new file mode 100644
index 00000000..5b0bc07e
--- /dev/null
+++ b/src/Ryujinx.Common/Memory/PartialUnmaps/PartialUnmapState.cs
@@ -0,0 +1,163 @@
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.Marshalling;
+using System.Runtime.Versioning;
+using System.Threading;
+
+using static Ryujinx.Common.Memory.PartialUnmaps.PartialUnmapHelpers;
+
+namespace Ryujinx.Common.Memory.PartialUnmaps
+{
+ /// <summary>
+ /// State for partial unmaps. Intended to be used on Windows.
+ /// </summary>
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public partial struct PartialUnmapState
+ {
+ public NativeReaderWriterLock PartialUnmapLock;
+ public int PartialUnmapsCount;
+ public ThreadLocalMap<int> LocalCounts;
+
+ public readonly static int PartialUnmapLockOffset;
+ public readonly static int PartialUnmapsCountOffset;
+ public readonly static int LocalCountsOffset;
+
+ public readonly static IntPtr GlobalState;
+
+ [SupportedOSPlatform("windows")]
+ [LibraryImport("kernel32.dll")]
+ private static partial int GetCurrentThreadId();
+
+ [SupportedOSPlatform("windows")]
+ [LibraryImport("kernel32.dll", SetLastError = true)]
+ private static partial IntPtr OpenThread(int dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, uint dwThreadId);
+
+ [SupportedOSPlatform("windows")]
+ [LibraryImport("kernel32.dll", SetLastError = true)]
+ [return: MarshalAs (UnmanagedType.Bool)]
+ private static partial bool CloseHandle(IntPtr hObject);
+
+ [SupportedOSPlatform("windows")]
+ [LibraryImport("kernel32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static partial bool GetExitCodeThread(IntPtr hThread, out uint lpExitCode);
+
+ /// <summary>
+ /// Creates a global static PartialUnmapState and populates the field offsets.
+ /// </summary>
+ static unsafe PartialUnmapState()
+ {
+ PartialUnmapState instance = new PartialUnmapState();
+
+ PartialUnmapLockOffset = OffsetOf(ref instance, ref instance.PartialUnmapLock);
+ PartialUnmapsCountOffset = OffsetOf(ref instance, ref instance.PartialUnmapsCount);
+ LocalCountsOffset = OffsetOf(ref instance, ref instance.LocalCounts);
+
+ int size = Unsafe.SizeOf<PartialUnmapState>();
+ GlobalState = Marshal.AllocHGlobal(size);
+ Unsafe.InitBlockUnaligned((void*)GlobalState, 0, (uint)size);
+ }
+
+ /// <summary>
+ /// Resets the global state.
+ /// </summary>
+ public static unsafe void Reset()
+ {
+ int size = Unsafe.SizeOf<PartialUnmapState>();
+ Unsafe.InitBlockUnaligned((void*)GlobalState, 0, (uint)size);
+ }
+
+ /// <summary>
+ /// Gets a reference to the global state.
+ /// </summary>
+ /// <returns>A reference to the global state</returns>
+ public static unsafe ref PartialUnmapState GetRef()
+ {
+ return ref Unsafe.AsRef<PartialUnmapState>((void*)GlobalState);
+ }
+
+ /// <summary>
+ /// Checks if an access violation handler should retry execution due to a fault caused by partial unmap.
+ /// </summary>
+ /// <remarks>
+ /// Due to Windows limitations, <see cref="UnmapView"/> might need to unmap more memory than requested.
+ /// The additional memory that was unmapped is later remapped, however this leaves a time gap where the
+ /// memory might be accessed but is unmapped. Users of the API must compensate for that by catching the
+ /// access violation and retrying if it happened between the unmap and remap operation.
+ /// This method can be used to decide if retrying in such cases is necessary or not.
+ ///
+ /// This version of the function is not used, but serves as a reference for the native
+ /// implementation in ARMeilleure.
+ /// </remarks>
+ /// <returns>True if execution should be retried, false otherwise</returns>
+ [SupportedOSPlatform("windows")]
+ public bool RetryFromAccessViolation()
+ {
+ PartialUnmapLock.AcquireReaderLock();
+
+ int threadID = GetCurrentThreadId();
+ int threadIndex = LocalCounts.GetOrReserve(threadID, 0);
+
+ if (threadIndex == -1)
+ {
+ // Out of thread local space... try again later.
+
+ PartialUnmapLock.ReleaseReaderLock();
+
+ return true;
+ }
+
+ ref int threadLocalPartialUnmapsCount = ref LocalCounts.GetValue(threadIndex);
+
+ bool retry = threadLocalPartialUnmapsCount != PartialUnmapsCount;
+ if (retry)
+ {
+ threadLocalPartialUnmapsCount = PartialUnmapsCount;
+ }
+
+ PartialUnmapLock.ReleaseReaderLock();
+
+ return retry;
+ }
+
+ /// <summary>
+ /// Iterates and trims threads in the thread -> count map that
+ /// are no longer active.
+ /// </summary>
+ [SupportedOSPlatform("windows")]
+ public void TrimThreads()
+ {
+ const uint ExitCodeStillActive = 259;
+ const int ThreadQueryInformation = 0x40;
+
+ Span<int> ids = LocalCounts.ThreadIds.AsSpan();
+
+ for (int i = 0; i < ids.Length; i++)
+ {
+ int id = ids[i];
+
+ if (id != 0)
+ {
+ IntPtr handle = OpenThread(ThreadQueryInformation, false, (uint)id);
+
+ if (handle == IntPtr.Zero)
+ {
+ Interlocked.CompareExchange(ref ids[i], 0, id);
+ }
+ else
+ {
+ GetExitCodeThread(handle, out uint exitCode);
+
+ if (exitCode != ExitCodeStillActive)
+ {
+ Interlocked.CompareExchange(ref ids[i], 0, id);
+ }
+
+ CloseHandle(handle);
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Memory/PartialUnmaps/ThreadLocalMap.cs b/src/Ryujinx.Common/Memory/PartialUnmaps/ThreadLocalMap.cs
new file mode 100644
index 00000000..a3bd5be8
--- /dev/null
+++ b/src/Ryujinx.Common/Memory/PartialUnmaps/ThreadLocalMap.cs
@@ -0,0 +1,92 @@
+using System.Runtime.InteropServices;
+using System.Threading;
+
+using static Ryujinx.Common.Memory.PartialUnmaps.PartialUnmapHelpers;
+
+namespace Ryujinx.Common.Memory.PartialUnmaps
+{
+ /// <summary>
+ /// A simple fixed size thread safe map that can be used from native code.
+ /// Integer thread IDs map to corresponding structs.
+ /// </summary>
+ /// <typeparam name="T">The value type for the map</typeparam>
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ThreadLocalMap<T> where T : unmanaged
+ {
+ public const int MapSize = 20;
+
+ public Array20<int> ThreadIds;
+ public Array20<T> Structs;
+
+ public static int ThreadIdsOffset;
+ public static int StructsOffset;
+
+ /// <summary>
+ /// Populates the field offsets for use when emitting native code.
+ /// </summary>
+ static ThreadLocalMap()
+ {
+ ThreadLocalMap<T> instance = new ThreadLocalMap<T>();
+
+ ThreadIdsOffset = OffsetOf(ref instance, ref instance.ThreadIds);
+ StructsOffset = OffsetOf(ref instance, ref instance.Structs);
+ }
+
+ /// <summary>
+ /// Gets the index of a given thread ID in the map, or reserves one.
+ /// When reserving a struct, its value is set to the given initial value.
+ /// Returns -1 when there is no space to reserve a new entry.
+ /// </summary>
+ /// <param name="threadId">Thread ID to use as a key</param>
+ /// <param name="initial">Initial value of the associated struct.</param>
+ /// <returns>The index of the entry, or -1 if none</returns>
+ public int GetOrReserve(int threadId, T initial)
+ {
+ // Try get a match first.
+
+ for (int i = 0; i < MapSize; i++)
+ {
+ int compare = Interlocked.CompareExchange(ref ThreadIds[i], threadId, threadId);
+
+ if (compare == threadId)
+ {
+ return i;
+ }
+ }
+
+ // Try get a free entry. Since the id is assumed to be unique to this thread, we know it doesn't exist yet.
+
+ for (int i = 0; i < MapSize; i++)
+ {
+ int compare = Interlocked.CompareExchange(ref ThreadIds[i], threadId, 0);
+
+ if (compare == 0)
+ {
+ Structs[i] = initial;
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ /// <summary>
+ /// Gets the struct value for a given map entry.
+ /// </summary>
+ /// <param name="index">Index of the entry</param>
+ /// <returns>A reference to the struct value</returns>
+ public ref T GetValue(int index)
+ {
+ return ref Structs[index];
+ }
+
+ /// <summary>
+ /// Releases an entry from the map.
+ /// </summary>
+ /// <param name="index">Index of the entry to release</param>
+ public void Release(int index)
+ {
+ Interlocked.Exchange(ref ThreadIds[index], 0);
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Memory/Ptr.cs b/src/Ryujinx.Common/Memory/Ptr.cs
new file mode 100644
index 00000000..66bcf569
--- /dev/null
+++ b/src/Ryujinx.Common/Memory/Ptr.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+
+namespace Ryujinx.Common.Memory
+{
+ /// <summary>
+ /// Represents a pointer to an unmanaged resource.
+ /// </summary>
+ /// <typeparam name="T">Type of the unmanaged resource</typeparam>
+ public unsafe struct Ptr<T> : IEquatable<Ptr<T>> where T : unmanaged
+ {
+ private IntPtr _ptr;
+
+ /// <summary>
+ /// Null pointer.
+ /// </summary>
+ public static Ptr<T> Null => new Ptr<T>() { _ptr = IntPtr.Zero };
+
+ /// <summary>
+ /// True if the pointer is null, false otherwise.
+ /// </summary>
+ public bool IsNull => _ptr == IntPtr.Zero;
+
+ /// <summary>
+ /// Gets a reference to the value.
+ /// </summary>
+ public ref T Value => ref Unsafe.AsRef<T>((void*)_ptr);
+
+ /// <summary>
+ /// Creates a new pointer to an unmanaged resource.
+ /// </summary>
+ /// <remarks>
+ /// For data on the heap, proper pinning is necessary during
+ /// use. Failure to do so will result in memory corruption and crashes.
+ /// </remarks>
+ /// <param name="value">Reference to the unmanaged resource</param>
+ public Ptr(ref T value)
+ {
+ _ptr = (IntPtr)Unsafe.AsPointer(ref value);
+ }
+
+ public override bool Equals(object obj)
+ {
+ return obj is Ptr<T> other && Equals(other);
+ }
+
+ public bool Equals([AllowNull] Ptr<T> other)
+ {
+ return _ptr == other._ptr;
+ }
+
+ public override int GetHashCode()
+ {
+ return _ptr.GetHashCode();
+ }
+
+ public static bool operator ==(Ptr<T> left, Ptr<T> right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(Ptr<T> left, Ptr<T> right)
+ {
+ return !(left == right);
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Memory/SpanOrArray.cs b/src/Ryujinx.Common/Memory/SpanOrArray.cs
new file mode 100644
index 00000000..a9798d27
--- /dev/null
+++ b/src/Ryujinx.Common/Memory/SpanOrArray.cs
@@ -0,0 +1,89 @@
+using System;
+
+namespace Ryujinx.Common.Memory
+{
+ /// <summary>
+ /// A struct that can represent both a Span and Array.
+ /// This is useful to keep the Array representation when possible to avoid copies.
+ /// </summary>
+ /// <typeparam name="T">Element Type</typeparam>
+ public readonly ref struct SpanOrArray<T> where T : unmanaged
+ {
+ public readonly T[] Array;
+ public readonly ReadOnlySpan<T> Span;
+
+ /// <summary>
+ /// Create a new SpanOrArray from an array.
+ /// </summary>
+ /// <param name="array">Array to store</param>
+ public SpanOrArray(T[] array)
+ {
+ Array = array;
+ Span = ReadOnlySpan<T>.Empty;
+ }
+
+ /// <summary>
+ /// Create a new SpanOrArray from a readonly span.
+ /// </summary>
+ /// <param name="array">Span to store</param>
+ public SpanOrArray(ReadOnlySpan<T> span)
+ {
+ Array = null;
+ Span = span;
+ }
+
+ /// <summary>
+ /// Return the contained array, or convert the span if necessary.
+ /// </summary>
+ /// <returns>An array containing the data</returns>
+ public T[] ToArray()
+ {
+ return Array ?? Span.ToArray();
+ }
+
+ /// <summary>
+ /// Return a ReadOnlySpan from either the array or ReadOnlySpan.
+ /// </summary>
+ /// <returns>A ReadOnlySpan containing the data</returns>
+ public ReadOnlySpan<T> AsSpan()
+ {
+ return Array ?? Span;
+ }
+
+ /// <summary>
+ /// Cast an array to a SpanOrArray.
+ /// </summary>
+ /// <param name="array">Source array</param>
+ public static implicit operator SpanOrArray<T>(T[] array)
+ {
+ return new SpanOrArray<T>(array);
+ }
+
+ /// <summary>
+ /// Cast a ReadOnlySpan to a SpanOrArray.
+ /// </summary>
+ /// <param name="span">Source ReadOnlySpan</param>
+ public static implicit operator SpanOrArray<T>(ReadOnlySpan<T> span)
+ {
+ return new SpanOrArray<T>(span);
+ }
+
+ /// <summary>
+ /// Cast a Span to a SpanOrArray.
+ /// </summary>
+ /// <param name="span">Source Span</param>
+ public static implicit operator SpanOrArray<T>(Span<T> span)
+ {
+ return new SpanOrArray<T>(span);
+ }
+
+ /// <summary>
+ /// Cast a SpanOrArray to a ReadOnlySpan
+ /// </summary>
+ /// <param name="spanOrArray">Source SpanOrArray</param>
+ public static implicit operator ReadOnlySpan<T>(SpanOrArray<T> spanOrArray)
+ {
+ return spanOrArray.AsSpan();
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Memory/SpanReader.cs b/src/Ryujinx.Common/Memory/SpanReader.cs
new file mode 100644
index 00000000..673932d0
--- /dev/null
+++ b/src/Ryujinx.Common/Memory/SpanReader.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Common.Memory
+{
+ public ref struct SpanReader
+ {
+ private ReadOnlySpan<byte> _input;
+
+ public int Length => _input.Length;
+
+ public SpanReader(ReadOnlySpan<byte> input)
+ {
+ _input = input;
+ }
+
+ public T Read<T>() where T : unmanaged
+ {
+ T value = MemoryMarshal.Cast<byte, T>(_input)[0];
+
+ _input = _input.Slice(Unsafe.SizeOf<T>());
+
+ return value;
+ }
+
+ public ReadOnlySpan<byte> GetSpan(int size)
+ {
+ ReadOnlySpan<byte> data = _input.Slice(0, size);
+
+ _input = _input.Slice(size);
+
+ return data;
+ }
+
+ public ReadOnlySpan<byte> GetSpanSafe(int size)
+ {
+ return GetSpan((int)Math.Min((uint)_input.Length, (uint)size));
+ }
+
+ public T ReadAt<T>(int offset) where T : unmanaged
+ {
+ return MemoryMarshal.Cast<byte, T>(_input.Slice(offset))[0];
+ }
+
+ public ReadOnlySpan<byte> GetSpanAt(int offset, int size)
+ {
+ return _input.Slice(offset, size);
+ }
+
+ public void Skip(int size)
+ {
+ _input = _input.Slice(size);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Memory/SpanWriter.cs b/src/Ryujinx.Common/Memory/SpanWriter.cs
new file mode 100644
index 00000000..5c35569d
--- /dev/null
+++ b/src/Ryujinx.Common/Memory/SpanWriter.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Common.Memory
+{
+ public ref struct SpanWriter
+ {
+ private Span<byte> _output;
+
+ public int Length => _output.Length;
+
+ public SpanWriter(Span<byte> output)
+ {
+ _output = output;
+ }
+
+ public void Write<T>(T value) where T : unmanaged
+ {
+ MemoryMarshal.Cast<byte, T>(_output)[0] = value;
+ _output = _output.Slice(Unsafe.SizeOf<T>());
+ }
+
+ public void Write(ReadOnlySpan<byte> data)
+ {
+ data.CopyTo(_output.Slice(0, data.Length));
+ _output = _output.Slice(data.Length);
+ }
+
+ public void WriteAt<T>(int offset, T value) where T : unmanaged
+ {
+ MemoryMarshal.Cast<byte, T>(_output.Slice(offset))[0] = value;
+ }
+
+ public void WriteAt(int offset, ReadOnlySpan<byte> data)
+ {
+ data.CopyTo(_output.Slice(offset, data.Length));
+ }
+
+ public void Skip(int size)
+ {
+ _output = _output.Slice(size);
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Memory/StructArrayHelpers.cs b/src/Ryujinx.Common/Memory/StructArrayHelpers.cs
new file mode 100644
index 00000000..a039d04e
--- /dev/null
+++ b/src/Ryujinx.Common/Memory/StructArrayHelpers.cs
@@ -0,0 +1,654 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Common.Memory
+{
+ public struct Array1<T> : IArray<T> where T : unmanaged
+ {
+ T _e0;
+ public int Length => 1;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 1);
+ }
+ public struct Array2<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array1<T> _other;
+#pragma warning restore CS0169
+ public int Length => 2;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 2);
+ }
+ public struct Array3<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array2<T> _other;
+#pragma warning restore CS0169
+ public int Length => 3;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 3);
+ }
+ public struct Array4<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array3<T> _other;
+#pragma warning restore CS0169
+ public int Length => 4;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 4);
+ }
+ public struct Array5<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array4<T> _other;
+#pragma warning restore CS0169
+ public int Length => 5;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 5);
+ }
+ public struct Array6<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array5<T> _other;
+#pragma warning restore CS0169
+ public int Length => 6;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 6);
+ }
+ public struct Array7<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array6<T> _other;
+#pragma warning restore CS0169
+ public int Length => 7;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 7);
+ }
+ public struct Array8<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array7<T> _other;
+#pragma warning restore CS0169
+ public int Length => 8;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 8);
+ }
+ public struct Array9<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array8<T> _other;
+#pragma warning restore CS0169
+ public int Length => 9;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 9);
+ }
+ public struct Array10<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array9<T> _other;
+#pragma warning restore CS0169
+ public int Length => 10;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 10);
+ }
+ public struct Array11<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array10<T> _other;
+#pragma warning restore CS0169
+ public int Length => 11;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 11);
+ }
+ public struct Array12<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array11<T> _other;
+#pragma warning restore CS0169
+ public int Length => 12;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 12);
+ }
+ public struct Array13<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array12<T> _other;
+#pragma warning restore CS0169
+ public int Length => 13;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 13);
+ }
+ public struct Array14<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array13<T> _other;
+#pragma warning restore CS0169
+ public int Length => 14;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 14);
+ }
+ public struct Array15<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array14<T> _other;
+#pragma warning restore CS0169
+ public int Length => 15;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 15);
+ }
+ public struct Array16<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array15<T> _other;
+#pragma warning restore CS0169
+ public int Length => 16;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 16);
+ }
+ public struct Array17<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array16<T> _other;
+#pragma warning restore CS0169
+ public int Length => 17;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 17);
+ }
+ public struct Array18<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array17<T> _other;
+#pragma warning restore CS0169
+ public int Length => 18;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 18);
+ }
+ public struct Array19<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array18<T> _other;
+#pragma warning restore CS0169
+ public int Length => 19;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 19);
+ }
+ public struct Array20<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array19<T> _other;
+#pragma warning restore CS0169
+ public int Length => 20;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 20);
+ }
+ public struct Array21<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array20<T> _other;
+#pragma warning restore CS0169
+ public int Length => 21;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 21);
+ }
+ public struct Array22<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array21<T> _other;
+#pragma warning restore CS0169
+ public int Length => 22;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 22);
+ }
+ public struct Array23<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array22<T> _other;
+#pragma warning restore CS0169
+ public int Length => 23;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 23);
+ }
+ public struct Array24<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array23<T> _other;
+#pragma warning restore CS0169
+ public int Length => 24;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 24);
+ }
+ public struct Array25<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array24<T> _other;
+#pragma warning restore CS0169
+ public int Length => 25;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 25);
+ }
+ public struct Array26<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array25<T> _other;
+#pragma warning restore CS0169
+ public int Length => 26;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 26);
+ }
+ public struct Array27<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array26<T> _other;
+#pragma warning restore CS0169
+ public int Length => 27;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 27);
+ }
+ public struct Array28<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array27<T> _other;
+#pragma warning restore CS0169
+ public int Length => 28;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 28);
+ }
+ public struct Array29<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array28<T> _other;
+#pragma warning restore CS0169
+ public int Length => 29;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 29);
+ }
+ public struct Array30<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array29<T> _other;
+#pragma warning restore CS0169
+ public int Length => 30;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 30);
+ }
+ public struct Array31<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array30<T> _other;
+#pragma warning restore CS0169
+ public int Length => 31;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 31);
+ }
+ public struct Array32<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array31<T> _other;
+#pragma warning restore CS0169
+ public int Length => 32;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 32);
+ }
+ public struct Array33<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array32<T> _other;
+#pragma warning restore CS0169
+ public int Length => 33;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 33);
+ }
+ public struct Array34<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array33<T> _other;
+#pragma warning restore CS0169
+ public int Length => 34;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 34);
+ }
+ public struct Array35<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array34<T> _other;
+#pragma warning restore CS0169
+ public int Length => 35;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 35);
+ }
+ public struct Array36<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array35<T> _other;
+#pragma warning restore CS0169
+ public int Length => 36;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 36);
+ }
+ public struct Array37<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array36<T> _other;
+#pragma warning restore CS0169
+ public int Length => 37;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 37);
+ }
+ public struct Array38<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array37<T> _other;
+#pragma warning restore CS0169
+ public int Length => 38;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 38);
+ }
+ public struct Array39<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array38<T> _other;
+#pragma warning restore CS0169
+ public int Length => 39;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 39);
+ }
+ public struct Array40<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array39<T> _other;
+#pragma warning restore CS0169
+ public int Length => 40;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 40);
+ }
+ public struct Array41<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array40<T> _other;
+#pragma warning restore CS0169
+ public int Length => 41;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 41);
+ }
+ public struct Array42<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array41<T> _other;
+#pragma warning restore CS0169
+ public int Length => 42;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 42);
+ }
+ public struct Array43<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array42<T> _other;
+#pragma warning restore CS0169
+ public int Length => 43;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 43);
+ }
+ public struct Array44<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array43<T> _other;
+#pragma warning restore CS0169
+ public int Length => 44;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 44);
+ }
+ public struct Array45<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array44<T> _other;
+#pragma warning restore CS0169
+ public int Length => 45;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 45);
+ }
+ public struct Array46<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array45<T> _other;
+#pragma warning restore CS0169
+ public int Length => 46;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 46);
+ }
+ public struct Array47<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array46<T> _other;
+#pragma warning restore CS0169
+ public int Length => 47;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 47);
+ }
+ public struct Array48<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array47<T> _other;
+#pragma warning restore CS0169
+ public int Length => 48;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 48);
+ }
+ public struct Array49<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array48<T> _other;
+#pragma warning restore CS0169
+ public int Length => 49;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 49);
+ }
+ public struct Array50<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array49<T> _other;
+#pragma warning restore CS0169
+ public int Length => 50;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 50);
+ }
+ public struct Array51<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array50<T> _other;
+#pragma warning restore CS0169
+ public int Length => 51;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 51);
+ }
+ public struct Array52<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array51<T> _other;
+#pragma warning restore CS0169
+ public int Length => 52;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 52);
+ }
+ public struct Array53<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array52<T> _other;
+#pragma warning restore CS0169
+ public int Length => 53;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 53);
+ }
+ public struct Array54<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array53<T> _other;
+#pragma warning restore CS0169
+ public int Length => 54;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 54);
+ }
+ public struct Array55<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array54<T> _other;
+#pragma warning restore CS0169
+ public int Length => 55;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 55);
+ }
+ public struct Array56<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array55<T> _other;
+#pragma warning restore CS0169
+ public int Length => 56;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 56);
+ }
+ public struct Array57<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array56<T> _other;
+#pragma warning restore CS0169
+ public int Length => 57;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 57);
+ }
+ public struct Array58<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array57<T> _other;
+#pragma warning restore CS0169
+ public int Length => 58;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 58);
+ }
+ public struct Array59<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array58<T> _other;
+#pragma warning restore CS0169
+ public int Length => 59;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 59);
+ }
+ public struct Array60<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array59<T> _other;
+#pragma warning restore CS0169
+ public int Length => 60;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 60);
+ }
+ public struct Array61<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array60<T> _other;
+#pragma warning restore CS0169
+ public int Length => 61;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 61);
+ }
+ public struct Array62<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array61<T> _other;
+#pragma warning restore CS0169
+ public int Length => 62;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 62);
+ }
+ public struct Array63<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array62<T> _other;
+#pragma warning restore CS0169
+ public int Length => 63;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 63);
+ }
+ public struct Array64<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array63<T> _other;
+#pragma warning restore CS0169
+ public int Length => 64;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 64);
+ }
+ public struct Array73<T> : IArray<T> where T : unmanaged
+ {
+#pragma warning disable CS0169
+ T _e0;
+ Array64<T> _other;
+ Array8<T> _other2;
+#pragma warning restore CS0169
+ public int Length => 73;
+ public ref T this[int index] => ref AsSpan()[index];
+ public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, 73);
+ }
+}
diff --git a/src/Ryujinx.Common/Memory/StructByteArrayHelpers.cs b/src/Ryujinx.Common/Memory/StructByteArrayHelpers.cs
new file mode 100644
index 00000000..8693f5b8
--- /dev/null
+++ b/src/Ryujinx.Common/Memory/StructByteArrayHelpers.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Common.Memory
+{
+ [StructLayout(LayoutKind.Sequential, Size = Size, Pack = 1)]
+ public struct ByteArray128 : IArray<byte>
+ {
+ private const int Size = 128;
+
+ byte _element;
+
+ public int Length => Size;
+ public ref byte this[int index] => ref AsSpan()[index];
+ public Span<byte> AsSpan() => MemoryMarshal.CreateSpan(ref _element, Size);
+ }
+
+ [StructLayout(LayoutKind.Sequential, Size = Size, Pack = 1)]
+ public struct ByteArray256 : IArray<byte>
+ {
+ private const int Size = 256;
+
+ byte _element;
+
+ public int Length => Size;
+ public ref byte this[int index] => ref AsSpan()[index];
+ public Span<byte> AsSpan() => MemoryMarshal.CreateSpan(ref _element, Size);
+ }
+
+ [StructLayout(LayoutKind.Sequential, Size = Size, Pack = 1)]
+ public struct ByteArray512 : IArray<byte>
+ {
+ private const int Size = 512;
+
+ byte _element;
+
+ public int Length => Size;
+ public ref byte this[int index] => ref AsSpan()[index];
+ public Span<byte> AsSpan() => MemoryMarshal.CreateSpan(ref _element, Size);
+ }
+
+ [StructLayout(LayoutKind.Sequential, Size = Size, Pack = 1)]
+ public struct ByteArray1024 : IArray<byte>
+ {
+ private const int Size = 1024;
+
+ byte _element;
+
+ public int Length => Size;
+ public ref byte this[int index] => ref AsSpan()[index];
+ public Span<byte> AsSpan() => MemoryMarshal.CreateSpan(ref _element, Size);
+ }
+
+ [StructLayout(LayoutKind.Sequential, Size = Size, Pack = 1)]
+ public struct ByteArray2048 : IArray<byte>
+ {
+ private const int Size = 2048;
+
+ byte _element;
+
+ public int Length => Size;
+ public ref byte this[int index] => ref AsSpan()[index];
+ public Span<byte> AsSpan() => MemoryMarshal.CreateSpan(ref _element, Size);
+ }
+
+ [StructLayout(LayoutKind.Sequential, Size = Size, Pack = 1)]
+ public struct ByteArray4096 : IArray<byte>
+ {
+ private const int Size = 4096;
+
+ byte _element;
+
+ public int Length => Size;
+ public ref byte this[int index] => ref AsSpan()[index];
+ public Span<byte> AsSpan() => MemoryMarshal.CreateSpan(ref _element, Size);
+ }
+}
diff --git a/src/Ryujinx.Common/PerformanceCounter.cs b/src/Ryujinx.Common/PerformanceCounter.cs
new file mode 100644
index 00000000..97ee23a2
--- /dev/null
+++ b/src/Ryujinx.Common/PerformanceCounter.cs
@@ -0,0 +1,82 @@
+using System.Diagnostics;
+
+namespace Ryujinx.Common
+{
+ public static class PerformanceCounter
+ {
+ private static double _ticksToNs;
+
+ /// <summary>
+ /// Represents the number of ticks in 1 day.
+ /// </summary>
+ public static long TicksPerDay { get; }
+
+ /// <summary>
+ /// Represents the number of ticks in 1 hour.
+ /// </summary>
+ public static long TicksPerHour { get; }
+
+ /// <summary>
+ /// Represents the number of ticks in 1 minute.
+ /// </summary>
+ public static long TicksPerMinute { get; }
+
+ /// <summary>
+ /// Represents the number of ticks in 1 second.
+ /// </summary>
+ public static long TicksPerSecond { get; }
+
+ /// <summary>
+ /// Represents the number of ticks in 1 millisecond.
+ /// </summary>
+ public static long TicksPerMillisecond { get; }
+
+ /// <summary>
+ /// Gets the number of ticks elapsed since the system started.
+ /// </summary>
+ public static long ElapsedTicks
+ {
+ get
+ {
+ return Stopwatch.GetTimestamp();
+ }
+ }
+
+ /// <summary>
+ /// Gets the number of milliseconds elapsed since the system started.
+ /// </summary>
+ public static long ElapsedMilliseconds
+ {
+ get
+ {
+ long timestamp = Stopwatch.GetTimestamp();
+
+ return timestamp / TicksPerMillisecond;
+ }
+ }
+
+ /// <summary>
+ /// Gets the number of nanoseconds elapsed since the system started.
+ /// </summary>
+ public static long ElapsedNanoseconds
+ {
+ get
+ {
+ long timestamp = Stopwatch.GetTimestamp();
+
+ return (long)(timestamp * _ticksToNs);
+ }
+ }
+
+ static PerformanceCounter()
+ {
+ TicksPerMillisecond = Stopwatch.Frequency / 1000;
+ TicksPerSecond = Stopwatch.Frequency;
+ TicksPerMinute = TicksPerSecond * 60;
+ TicksPerHour = TicksPerMinute * 60;
+ TicksPerDay = TicksPerHour * 24;
+
+ _ticksToNs = 1000000000.0 / Stopwatch.Frequency;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Pools/ObjectPool.cs b/src/Ryujinx.Common/Pools/ObjectPool.cs
new file mode 100644
index 00000000..e0bf5df6
--- /dev/null
+++ b/src/Ryujinx.Common/Pools/ObjectPool.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Threading;
+
+namespace Ryujinx.Common
+{
+ public class ObjectPool<T>
+ where T : class
+ {
+ private T _firstItem;
+ private readonly T[] _items;
+
+ private readonly Func<T> _factory;
+
+ public ObjectPool(Func<T> factory, int size)
+ {
+ _items = new T[size - 1];
+ _factory = factory;
+ }
+
+ public T Allocate()
+ {
+ T instance = _firstItem;
+
+ if (instance == null || instance != Interlocked.CompareExchange(ref _firstItem, null, instance))
+ {
+ instance = AllocateInternal();
+ }
+
+ return instance;
+ }
+
+ private T AllocateInternal()
+ {
+ T[] items = _items;
+
+ for (int i = 0; i < items.Length; i++)
+ {
+ T instance = items[i];
+
+ if (instance != null && instance == Interlocked.CompareExchange(ref items[i], null, instance))
+ {
+ return instance;
+ }
+ }
+
+ return _factory();
+ }
+
+ public void Release(T obj)
+ {
+ if (_firstItem == null)
+ {
+ _firstItem = obj;
+ }
+ else
+ {
+ ReleaseInternal(obj);
+ }
+ }
+
+ private void ReleaseInternal(T obj)
+ {
+ T[] items = _items;
+
+ for (int i = 0; i < items.Length; i++)
+ {
+ if (items[i] == null)
+ {
+ items[i] = obj;
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Pools/SharedPools.cs b/src/Ryujinx.Common/Pools/SharedPools.cs
new file mode 100644
index 00000000..b4860b85
--- /dev/null
+++ b/src/Ryujinx.Common/Pools/SharedPools.cs
@@ -0,0 +1,17 @@
+namespace Ryujinx.Common
+{
+ public static class SharedPools
+ {
+ private static class DefaultPool<T>
+ where T : class, new()
+ {
+ public static readonly ObjectPool<T> Instance = new ObjectPool<T>(() => new T(), 20);
+ }
+
+ public static ObjectPool<T> Default<T>()
+ where T : class, new()
+ {
+ return DefaultPool<T>.Instance;
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Pools/ThreadStaticArray.cs b/src/Ryujinx.Common/Pools/ThreadStaticArray.cs
new file mode 100644
index 00000000..21434a02
--- /dev/null
+++ b/src/Ryujinx.Common/Pools/ThreadStaticArray.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace Ryujinx.Common.Pools
+{
+ public static class ThreadStaticArray<T>
+ {
+ [ThreadStatic]
+ private static T[] _array;
+
+ public static ref T[] Get()
+ {
+ if (_array == null)
+ {
+ _array = new T[1];
+ }
+
+ return ref _array;
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/ReactiveObject.cs b/src/Ryujinx.Common/ReactiveObject.cs
new file mode 100644
index 00000000..44897f26
--- /dev/null
+++ b/src/Ryujinx.Common/ReactiveObject.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Threading;
+
+namespace Ryujinx.Common
+{
+ public class ReactiveObject<T>
+ {
+ private ReaderWriterLock _readerWriterLock = new ReaderWriterLock();
+ private bool _isInitialized = false;
+ private T _value;
+
+ public event EventHandler<ReactiveEventArgs<T>> Event;
+
+ public T Value
+ {
+ get
+ {
+ _readerWriterLock.AcquireReaderLock(Timeout.Infinite);
+ T value = _value;
+ _readerWriterLock.ReleaseReaderLock();
+
+ return value;
+ }
+ set
+ {
+ _readerWriterLock.AcquireWriterLock(Timeout.Infinite);
+
+ T oldValue = _value;
+
+ bool oldIsInitialized = _isInitialized;
+
+ _isInitialized = true;
+ _value = value;
+
+ _readerWriterLock.ReleaseWriterLock();
+
+ if (!oldIsInitialized || oldValue == null || !oldValue.Equals(_value))
+ {
+ Event?.Invoke(this, new ReactiveEventArgs<T>(oldValue, value));
+ }
+ }
+ }
+
+ public static implicit operator T(ReactiveObject<T> obj)
+ {
+ return obj.Value;
+ }
+ }
+
+ public class ReactiveEventArgs<T>
+ {
+ public T OldValue { get; }
+ public T NewValue { get; }
+
+ public ReactiveEventArgs(T oldValue, T newValue)
+ {
+ OldValue = oldValue;
+ NewValue = newValue;
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/ReferenceEqualityComparer.cs b/src/Ryujinx.Common/ReferenceEqualityComparer.cs
new file mode 100644
index 00000000..d8ec29d8
--- /dev/null
+++ b/src/Ryujinx.Common/ReferenceEqualityComparer.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Ryujinx.Common
+{
+ public class ReferenceEqualityComparer<T> : IEqualityComparer<T>
+ where T : class
+ {
+ public bool Equals(T x, T y)
+ {
+ return x == y;
+ }
+
+ public int GetHashCode([DisallowNull] T obj)
+ {
+ return obj.GetHashCode();
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/ReleaseInformation.cs b/src/Ryujinx.Common/ReleaseInformation.cs
new file mode 100644
index 00000000..601c05b1
--- /dev/null
+++ b/src/Ryujinx.Common/ReleaseInformation.cs
@@ -0,0 +1,60 @@
+using Ryujinx.Common.Configuration;
+using System;
+using System.Reflection;
+
+namespace Ryujinx.Common
+{
+ // DO NOT EDIT, filled by CI
+ public static class ReleaseInformation
+ {
+ private const string FlatHubChannelOwner = "flathub";
+
+ public static string BuildVersion = "%%RYUJINX_BUILD_VERSION%%";
+ public static string BuildGitHash = "%%RYUJINX_BUILD_GIT_HASH%%";
+ public static string ReleaseChannelName = "%%RYUJINX_TARGET_RELEASE_CHANNEL_NAME%%";
+ public static string ReleaseChannelOwner = "%%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER%%";
+ public static string ReleaseChannelRepo = "%%RYUJINX_TARGET_RELEASE_CHANNEL_REPO%%";
+
+ public static bool IsValid()
+ {
+ return !BuildGitHash.StartsWith("%%") &&
+ !ReleaseChannelName.StartsWith("%%") &&
+ !ReleaseChannelOwner.StartsWith("%%") &&
+ !ReleaseChannelRepo.StartsWith("%%");
+ }
+
+ public static bool IsFlatHubBuild()
+ {
+ return IsValid() && ReleaseChannelOwner.Equals(FlatHubChannelOwner);
+ }
+
+ public static string GetVersion()
+ {
+ if (IsValid())
+ {
+ return BuildVersion;
+ }
+ else
+ {
+ return Assembly.GetEntryAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion;
+ }
+ }
+
+#if FORCE_EXTERNAL_BASE_DIR
+ public static string GetBaseApplicationDirectory()
+ {
+ return AppDataManager.BaseDirPath;
+ }
+#else
+ public static string GetBaseApplicationDirectory()
+ {
+ if (IsFlatHubBuild() || OperatingSystem.IsMacOS())
+ {
+ return AppDataManager.BaseDirPath;
+ }
+
+ return AppDomain.CurrentDomain.BaseDirectory;
+ }
+#endif
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Ryujinx.Common.csproj b/src/Ryujinx.Common/Ryujinx.Common.csproj
new file mode 100644
index 00000000..c02b11e0
--- /dev/null
+++ b/src/Ryujinx.Common/Ryujinx.Common.csproj
@@ -0,0 +1,15 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net7.0</TargetFramework>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ <DefineConstants Condition=" '$(ExtraDefineConstants)' != '' ">$(DefineConstants);$(ExtraDefineConstants)</DefineConstants>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.IO.RecyclableMemoryStream" />
+ <PackageReference Include="MsgPack.Cli" />
+ <PackageReference Include="System.Management" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Ryujinx.Common/SystemInfo/LinuxSystemInfo.cs b/src/Ryujinx.Common/SystemInfo/LinuxSystemInfo.cs
new file mode 100644
index 00000000..b0c15e49
--- /dev/null
+++ b/src/Ryujinx.Common/SystemInfo/LinuxSystemInfo.cs
@@ -0,0 +1,80 @@
+using Ryujinx.Common.Logging;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Common.SystemInfo
+{
+ [SupportedOSPlatform("linux")]
+ class LinuxSystemInfo : SystemInfo
+ {
+ internal LinuxSystemInfo()
+ {
+ string cpuName = GetCpuidCpuName();
+
+ if (cpuName == null)
+ {
+ var cpuDict = new Dictionary<string, string>(StringComparer.Ordinal)
+ {
+ ["model name"] = null,
+ ["Processor"] = null,
+ ["Hardware"] = null
+ };
+
+ ParseKeyValues("/proc/cpuinfo", cpuDict);
+
+ cpuName = cpuDict["model name"] ?? cpuDict["Processor"] ?? cpuDict["Hardware"] ?? "Unknown";
+ }
+
+ var memDict = new Dictionary<string, string>(StringComparer.Ordinal)
+ {
+ ["MemTotal"] = null,
+ ["MemAvailable"] = null
+ };
+
+ ParseKeyValues("/proc/meminfo", memDict);
+
+ // Entries are in KiB
+ ulong.TryParse(memDict["MemTotal"]?.Split(' ')[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong totalKiB);
+ ulong.TryParse(memDict["MemAvailable"]?.Split(' ')[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong availableKiB);
+
+ CpuName = $"{cpuName} ; {LogicalCoreCount} logical";
+ RamTotal = totalKiB * 1024;
+ RamAvailable = availableKiB * 1024;
+ }
+
+ private static void ParseKeyValues(string filePath, Dictionary<string, string> itemDict)
+ {
+ if (!File.Exists(filePath))
+ {
+ Logger.Error?.Print(LogClass.Application, $"File \"{filePath}\" not found");
+
+ return;
+ }
+
+ int count = itemDict.Count;
+
+ using (StreamReader file = new StreamReader(filePath))
+ {
+ string line;
+ while ((line = file.ReadLine()) != null)
+ {
+ string[] kvPair = line.Split(':', 2, StringSplitOptions.TrimEntries);
+
+ if (kvPair.Length < 2) continue;
+
+ string key = kvPair[0];
+
+ if (itemDict.TryGetValue(key, out string value) && value == null)
+ {
+ itemDict[key] = kvPair[1];
+
+ if (--count <= 0) break;
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/SystemInfo/MacOSSystemInfo.cs b/src/Ryujinx.Common/SystemInfo/MacOSSystemInfo.cs
new file mode 100644
index 00000000..06324a54
--- /dev/null
+++ b/src/Ryujinx.Common/SystemInfo/MacOSSystemInfo.cs
@@ -0,0 +1,157 @@
+using Ryujinx.Common.Logging;
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+using System.Text;
+
+namespace Ryujinx.Common.SystemInfo
+{
+ [SupportedOSPlatform("macos")]
+ partial class MacOSSystemInfo : SystemInfo
+ {
+ internal MacOSSystemInfo()
+ {
+ string cpuName = GetCpuidCpuName();
+
+ if (cpuName == null && sysctlbyname("machdep.cpu.brand_string", out cpuName) != 0)
+ {
+ cpuName = "Unknown";
+ }
+
+ ulong totalRAM = 0;
+
+ if (sysctlbyname("hw.memsize", ref totalRAM) != 0) // Bytes
+ {
+ totalRAM = 0;
+ }
+
+ CpuName = $"{cpuName} ; {LogicalCoreCount} logical";
+ RamTotal = totalRAM;
+ RamAvailable = GetVMInfoAvailableMemory();
+ }
+
+ static ulong GetVMInfoAvailableMemory()
+ {
+ var port = mach_host_self();
+
+ uint pageSize = 0;
+ var result = host_page_size(port, ref pageSize);
+
+ if (result != 0)
+ {
+ Logger.Error?.Print(LogClass.Application, $"Failed to query Available RAM. host_page_size() error = {result}");
+ return 0;
+ }
+
+ const int flavor = 4; // HOST_VM_INFO64
+ uint count = (uint)(Marshal.SizeOf<VMStatistics64>() / sizeof(int)); // HOST_VM_INFO64_COUNT
+ VMStatistics64 stats = new();
+ result = host_statistics64(port, flavor, ref stats, ref count);
+
+ if (result != 0)
+ {
+ Logger.Error?.Print(LogClass.Application, $"Failed to query Available RAM. host_statistics64() error = {result}");
+ return 0;
+ }
+
+ return (ulong)(stats.FreeCount + stats.InactiveCount) * pageSize;
+ }
+
+ private const string SystemLibraryName = "libSystem.dylib";
+
+ [LibraryImport(SystemLibraryName, SetLastError = true)]
+ private static partial int sysctlbyname([MarshalAs(UnmanagedType.LPStr)] string name, IntPtr oldValue, ref ulong oldSize, IntPtr newValue, ulong newValueSize);
+
+ private static int sysctlbyname(string name, IntPtr oldValue, ref ulong oldSize)
+ {
+ if (sysctlbyname(name, oldValue, ref oldSize, IntPtr.Zero, 0) == -1)
+ {
+ int err = Marshal.GetLastWin32Error();
+
+ Logger.Error?.Print(LogClass.Application, $"Cannot retrieve '{name}'. Error Code {err}");
+
+ return err;
+ }
+
+ return 0;
+ }
+
+ private static int sysctlbyname<T>(string name, ref T oldValue)
+ {
+ unsafe
+ {
+ ulong oldValueSize = (ulong)Unsafe.SizeOf<T>();
+
+ return sysctlbyname(name, (IntPtr)Unsafe.AsPointer(ref oldValue), ref oldValueSize);
+ }
+ }
+
+ private static int sysctlbyname(string name, out string oldValue)
+ {
+ oldValue = default;
+
+ ulong strSize = 0;
+
+ int res = sysctlbyname(name, IntPtr.Zero, ref strSize);
+
+ if (res == 0)
+ {
+ byte[] rawData = new byte[strSize];
+
+ unsafe
+ {
+ fixed (byte* rawDataPtr = rawData)
+ {
+ res = sysctlbyname(name, (IntPtr)rawDataPtr, ref strSize);
+ }
+
+ if (res == 0)
+ {
+ oldValue = Encoding.ASCII.GetString(rawData);
+ }
+ }
+ }
+
+ return res;
+ }
+
+ [LibraryImport(SystemLibraryName, SetLastError = true)]
+ private static partial uint mach_host_self();
+
+ [LibraryImport(SystemLibraryName, SetLastError = true)]
+ private static partial int host_page_size(uint host, ref uint out_page_size);
+
+ [StructLayout(LayoutKind.Sequential, Pack = 8)]
+ struct VMStatistics64
+ {
+ public uint FreeCount;
+ public uint ActiveCount;
+ public uint InactiveCount;
+ public uint WireCount;
+ public ulong ZeroFillCount;
+ public ulong Reactivations;
+ public ulong Pageins;
+ public ulong Pageouts;
+ public ulong Faults;
+ public ulong CowFaults;
+ public ulong Lookups;
+ public ulong Hits;
+ public ulong Purges;
+ public uint PurgeableCount;
+ public uint SpeculativeCount;
+ public ulong Decompressions;
+ public ulong Compressions;
+ public ulong Swapins;
+ public ulong Swapouts;
+ public uint CompressorPageCount;
+ public uint ThrottledCount;
+ public uint ExternalPageCount;
+ public uint InternalPageCount;
+ public ulong TotalUncompressedPagesInCompressor;
+ }
+
+ [LibraryImport(SystemLibraryName, SetLastError = true)]
+ private static partial int host_statistics64(uint host_priv, int host_flavor, ref VMStatistics64 host_info64_out, ref uint host_info64_outCnt);
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/SystemInfo/SystemInfo.cs b/src/Ryujinx.Common/SystemInfo/SystemInfo.cs
new file mode 100644
index 00000000..e9ce3c58
--- /dev/null
+++ b/src/Ryujinx.Common/SystemInfo/SystemInfo.cs
@@ -0,0 +1,80 @@
+using Ryujinx.Common.Logging;
+using System;
+using System.Runtime.InteropServices;
+using System.Runtime.Intrinsics.X86;
+using System.Text;
+
+namespace Ryujinx.Common.SystemInfo
+{
+ public class SystemInfo
+ {
+ public string OsDescription { get; protected set; }
+ public string CpuName { get; protected set; }
+ public ulong RamTotal { get; protected set; }
+ public ulong RamAvailable { get; protected set; }
+ protected static int LogicalCoreCount => Environment.ProcessorCount;
+
+ protected SystemInfo()
+ {
+ OsDescription = $"{RuntimeInformation.OSDescription} ({RuntimeInformation.OSArchitecture})";
+ CpuName = "Unknown";
+ }
+
+ private static string ToMiBString(ulong bytesValue) => (bytesValue == 0) ? "Unknown" : $"{bytesValue / 1024 / 1024} MiB";
+
+ public void Print()
+ {
+ Logger.Notice.Print(LogClass.Application, $"Operating System: {OsDescription}");
+ Logger.Notice.Print(LogClass.Application, $"CPU: {CpuName}");
+ Logger.Notice.Print(LogClass.Application, $"RAM: Total {ToMiBString(RamTotal)} ; Available {ToMiBString(RamAvailable)}");
+ }
+
+ public static SystemInfo Gather()
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ return new WindowsSystemInfo();
+ }
+ else if (OperatingSystem.IsLinux())
+ {
+ return new LinuxSystemInfo();
+ }
+ else if (OperatingSystem.IsMacOS())
+ {
+ return new MacOSSystemInfo();
+ }
+ else
+ {
+ Logger.Error?.Print(LogClass.Application, "SystemInfo unsupported on this platform");
+
+ return new SystemInfo();
+ }
+ }
+
+ // x86 exposes a 48 byte ASCII "CPU brand" string via CPUID leaves 0x80000002-0x80000004.
+ internal static string GetCpuidCpuName()
+ {
+ if (!X86Base.IsSupported)
+ {
+ return null;
+ }
+
+ // Check if CPU supports the query
+ if ((uint)X86Base.CpuId(unchecked((int)0x80000000), 0).Eax < 0x80000004)
+ {
+ return null;
+ }
+
+ int[] regs = new int[12];
+
+ for (uint i = 0; i < 3; ++i)
+ {
+ (regs[4 * i], regs[4 * i + 1], regs[4 * i + 2], regs[4 * i + 3]) = X86Base.CpuId((int)(0x80000002 + i), 0);
+ }
+
+ string name = Encoding.ASCII.GetString(MemoryMarshal.Cast<int, byte>(regs)).Replace('\0', ' ').Trim();
+
+ return string.IsNullOrEmpty(name) ? null : name;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/SystemInfo/WindowsSystemInfo.cs b/src/Ryujinx.Common/SystemInfo/WindowsSystemInfo.cs
new file mode 100644
index 00000000..c3598a1e
--- /dev/null
+++ b/src/Ryujinx.Common/SystemInfo/WindowsSystemInfo.cs
@@ -0,0 +1,89 @@
+using Ryujinx.Common.Logging;
+using System;
+using System.Management;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Common.SystemInfo
+{
+ [SupportedOSPlatform("windows")]
+ partial class WindowsSystemInfo : SystemInfo
+ {
+ internal WindowsSystemInfo()
+ {
+ CpuName = $"{GetCpuidCpuName() ?? GetCpuNameWMI()} ; {LogicalCoreCount} logical"; // WMI is very slow
+ (RamTotal, RamAvailable) = GetMemoryStats();
+ }
+
+ private static (ulong Total, ulong Available) GetMemoryStats()
+ {
+ MemoryStatusEx memStatus = new MemoryStatusEx();
+ if (GlobalMemoryStatusEx(ref memStatus))
+ {
+ return (memStatus.TotalPhys, memStatus.AvailPhys); // Bytes
+ }
+ else
+ {
+ Logger.Error?.Print(LogClass.Application, $"GlobalMemoryStatusEx failed. Error {Marshal.GetLastWin32Error():X}");
+ }
+
+ return (0, 0);
+ }
+
+ private static string GetCpuNameWMI()
+ {
+ ManagementObjectCollection cpuObjs = GetWMIObjects("root\\CIMV2", "SELECT * FROM Win32_Processor");
+
+ if (cpuObjs != null)
+ {
+ foreach (var cpuObj in cpuObjs)
+ {
+ return cpuObj["Name"].ToString().Trim();
+ }
+ }
+
+ return Environment.GetEnvironmentVariable("PROCESSOR_IDENTIFIER").Trim();
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct MemoryStatusEx
+ {
+ public uint Length;
+ public uint MemoryLoad;
+ public ulong TotalPhys;
+ public ulong AvailPhys;
+ public ulong TotalPageFile;
+ public ulong AvailPageFile;
+ public ulong TotalVirtual;
+ public ulong AvailVirtual;
+ public ulong AvailExtendedVirtual;
+
+ public MemoryStatusEx()
+ {
+ Length = (uint)Marshal.SizeOf<MemoryStatusEx>();
+ }
+ }
+
+ [LibraryImport("kernel32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static partial bool GlobalMemoryStatusEx(ref MemoryStatusEx lpBuffer);
+
+ private static ManagementObjectCollection GetWMIObjects(string scope, string query)
+ {
+ try
+ {
+ return new ManagementObjectSearcher(scope, query).Get();
+ }
+ catch (PlatformNotSupportedException ex)
+ {
+ Logger.Error?.Print(LogClass.Application, $"WMI isn't available : {ex.Message}");
+ }
+ catch (COMException ex)
+ {
+ Logger.Error?.Print(LogClass.Application, $"WMI isn't available : {ex.Message}");
+ }
+
+ return null;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/SystemInterop/DisplaySleep.cs b/src/Ryujinx.Common/SystemInterop/DisplaySleep.cs
new file mode 100644
index 00000000..5a1f66f5
--- /dev/null
+++ b/src/Ryujinx.Common/SystemInterop/DisplaySleep.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Common.SystemInterop
+{
+ public partial class DisplaySleep
+ {
+ [Flags]
+ enum EXECUTION_STATE : uint
+ {
+ ES_CONTINUOUS = 0x80000000,
+ ES_DISPLAY_REQUIRED = 0x00000002,
+ ES_SYSTEM_REQUIRED = 0x00000001
+ }
+
+ [LibraryImport("kernel32.dll", SetLastError = true)]
+ private static partial EXECUTION_STATE SetThreadExecutionState(EXECUTION_STATE esFlags);
+
+ static public void Prevent()
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS | EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_DISPLAY_REQUIRED);
+ }
+ }
+
+ static public void Restore()
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS);
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/SystemInterop/ForceDpiAware.cs b/src/Ryujinx.Common/SystemInterop/ForceDpiAware.cs
new file mode 100644
index 00000000..f17612a6
--- /dev/null
+++ b/src/Ryujinx.Common/SystemInterop/ForceDpiAware.cs
@@ -0,0 +1,96 @@
+using Ryujinx.Common.Logging;
+using System;
+using System.Globalization;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Common.SystemInterop
+{
+ public static partial class ForceDpiAware
+ {
+ [LibraryImport("user32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static partial bool SetProcessDPIAware();
+
+ private const string X11LibraryName = "libX11.so.6";
+
+ [LibraryImport(X11LibraryName)]
+ private static partial IntPtr XOpenDisplay([MarshalAs(UnmanagedType.LPStr)] string display);
+
+ [LibraryImport(X11LibraryName)]
+ private static partial IntPtr XGetDefault(IntPtr display, [MarshalAs(UnmanagedType.LPStr)] string program, [MarshalAs(UnmanagedType.LPStr)] string option);
+
+ [LibraryImport(X11LibraryName)]
+ private static partial int XDisplayWidth(IntPtr display, int screenNumber);
+
+ [LibraryImport(X11LibraryName)]
+ private static partial int XDisplayWidthMM(IntPtr display, int screenNumber);
+
+ [LibraryImport(X11LibraryName)]
+ private static partial int XCloseDisplay(IntPtr display);
+
+ private static readonly double _standardDpiScale = 96.0;
+ private static readonly double _maxScaleFactor = 1.25;
+
+ /// <summary>
+ /// Marks the application as DPI-Aware when running on the Windows operating system.
+ /// </summary>
+ public static void Windows()
+ {
+ // Make process DPI aware for proper window sizing on high-res screens.
+ if (OperatingSystem.IsWindowsVersionAtLeast(6))
+ {
+ SetProcessDPIAware();
+ }
+ }
+
+ public static double GetActualScaleFactor()
+ {
+ double userDpiScale = 96.0;
+
+ try
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ userDpiScale = GdiPlusHelper.GetDpiX(IntPtr.Zero);
+ }
+ else if (OperatingSystem.IsLinux())
+ {
+ string xdgSessionType = Environment.GetEnvironmentVariable("XDG_SESSION_TYPE")?.ToLower();
+
+ if (xdgSessionType == null || xdgSessionType == "x11")
+ {
+ IntPtr display = XOpenDisplay(null);
+ string dpiString = Marshal.PtrToStringAnsi(XGetDefault(display, "Xft", "dpi"));
+ if (dpiString == null || !double.TryParse(dpiString, NumberStyles.Any, CultureInfo.InvariantCulture, out userDpiScale))
+ {
+ userDpiScale = (double)XDisplayWidth(display, 0) * 25.4 / (double)XDisplayWidthMM(display, 0);
+ }
+ XCloseDisplay(display);
+ }
+ else if (xdgSessionType == "wayland")
+ {
+ // TODO
+ Logger.Warning?.Print(LogClass.Application, $"Couldn't determine monitor DPI: Wayland not yet supported");
+ }
+ else
+ {
+ Logger.Warning?.Print(LogClass.Application, $"Couldn't determine monitor DPI: Unrecognised XDG_SESSION_TYPE: {xdgSessionType}");
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ Logger.Warning?.Print(LogClass.Application, $"Couldn't determine monitor DPI: {e.Message}");
+ }
+
+ return userDpiScale;
+ }
+
+ public static double GetWindowScaleFactor()
+ {
+ double userDpiScale = GetActualScaleFactor();
+
+ return Math.Min(userDpiScale / _standardDpiScale, _maxScaleFactor);
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/SystemInterop/GdiPlusHelper.cs b/src/Ryujinx.Common/SystemInterop/GdiPlusHelper.cs
new file mode 100644
index 00000000..1001424d
--- /dev/null
+++ b/src/Ryujinx.Common/SystemInterop/GdiPlusHelper.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Common.SystemInterop
+{
+ [SupportedOSPlatform("windows")]
+ public static partial class GdiPlusHelper
+ {
+ private const string LibraryName = "gdiplus.dll";
+
+ private static readonly IntPtr _initToken;
+
+ static GdiPlusHelper()
+ {
+ CheckStatus(GdiplusStartup(out _initToken, StartupInputEx.Default, out _));
+ }
+
+ private static void CheckStatus(int gdiStatus)
+ {
+ if (gdiStatus != 0)
+ {
+ throw new Exception($"GDI Status Error: {gdiStatus}");
+ }
+ }
+
+ private struct StartupInputEx
+ {
+ public int GdiplusVersion;
+
+#pragma warning disable CS0649
+ public IntPtr DebugEventCallback;
+ public int SuppressBackgroundThread;
+ public int SuppressExternalCodecs;
+ public int StartupParameters;
+#pragma warning restore CS0649
+
+ public static StartupInputEx Default => new StartupInputEx
+ {
+ // We assume Windows 8 and upper
+ GdiplusVersion = 2,
+ DebugEventCallback = IntPtr.Zero,
+ SuppressBackgroundThread = 0,
+ SuppressExternalCodecs = 0,
+ StartupParameters = 0,
+ };
+ }
+
+ private struct StartupOutput
+ {
+ public IntPtr NotificationHook;
+ public IntPtr NotificationUnhook;
+ }
+
+ [LibraryImport(LibraryName)]
+ private static partial int GdiplusStartup(out IntPtr token, in StartupInputEx input, out StartupOutput output);
+
+ [LibraryImport(LibraryName)]
+ private static partial int GdipCreateFromHWND(IntPtr hwnd, out IntPtr graphics);
+
+ [LibraryImport(LibraryName)]
+ private static partial int GdipDeleteGraphics(IntPtr graphics);
+
+ [LibraryImport(LibraryName)]
+ private static partial int GdipGetDpiX(IntPtr graphics, out float dpi);
+
+ public static float GetDpiX(IntPtr hwnd)
+ {
+ CheckStatus(GdipCreateFromHWND(hwnd, out IntPtr graphicsHandle));
+ CheckStatus(GdipGetDpiX(graphicsHandle, out float result));
+ CheckStatus(GdipDeleteGraphics(graphicsHandle));
+
+ return result;
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/SystemInterop/StdErrAdapter.cs b/src/Ryujinx.Common/SystemInterop/StdErrAdapter.cs
new file mode 100644
index 00000000..b1ed7b68
--- /dev/null
+++ b/src/Ryujinx.Common/SystemInterop/StdErrAdapter.cs
@@ -0,0 +1,109 @@
+using Microsoft.Win32.SafeHandles;
+using Ryujinx.Common.Logging;
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Ryujinx.Common.SystemInterop
+{
+ public partial class StdErrAdapter : IDisposable
+ {
+ private bool _disposable = false;
+ private Stream _pipeReader;
+ private Stream _pipeWriter;
+ private CancellationTokenSource _cancellationTokenSource;
+ private Task _worker;
+
+ public StdErrAdapter()
+ {
+ if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
+ {
+ RegisterPosix();
+ }
+ }
+
+ [SupportedOSPlatform("linux")]
+ [SupportedOSPlatform("macos")]
+ private void RegisterPosix()
+ {
+ const int stdErrFileno = 2;
+
+ (int readFd, int writeFd) = MakePipe();
+ dup2(writeFd, stdErrFileno);
+
+ _pipeReader = CreateFileDescriptorStream(readFd);
+ _pipeWriter = CreateFileDescriptorStream(writeFd);
+
+ _cancellationTokenSource = new CancellationTokenSource();
+ _worker = Task.Run(async () => await EventWorkerAsync(_cancellationTokenSource.Token), _cancellationTokenSource.Token);
+ _disposable = true;
+ }
+
+ [SupportedOSPlatform("linux")]
+ [SupportedOSPlatform("macos")]
+ private async Task EventWorkerAsync(CancellationToken cancellationToken)
+ {
+ using TextReader reader = new StreamReader(_pipeReader, leaveOpen: true);
+ string line;
+ while (cancellationToken.IsCancellationRequested == false && (line = await reader.ReadLineAsync(cancellationToken)) != null)
+ {
+ Logger.Error?.PrintRawMsg(line);
+ }
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (_disposable)
+ {
+ _disposable = false;
+
+ if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
+ {
+ _cancellationTokenSource.Cancel();
+ _worker.Wait(0);
+ _pipeReader?.Close();
+ _pipeWriter?.Close();
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+
+ [LibraryImport("libc", SetLastError = true)]
+ private static partial int dup2(int fd, int fd2);
+
+ [LibraryImport("libc", SetLastError = true)]
+ private static partial int pipe(Span<int> pipefd);
+
+ private static (int, int) MakePipe()
+ {
+ Span<int> pipefd = stackalloc int[2];
+
+ if (pipe(pipefd) == 0)
+ {
+ return (pipefd[0], pipefd[1]);
+ }
+ else
+ {
+ throw new();
+ }
+ }
+
+ [SupportedOSPlatform("linux")]
+ [SupportedOSPlatform("macos")]
+ private static Stream CreateFileDescriptorStream(int fd)
+ {
+ return new FileStream(
+ new SafeFileHandle((IntPtr)fd, ownsHandle: true),
+ FileAccess.ReadWrite
+ );
+ }
+
+ }
+}
diff --git a/src/Ryujinx.Common/SystemInterop/WindowsMultimediaTimerResolution.cs b/src/Ryujinx.Common/SystemInterop/WindowsMultimediaTimerResolution.cs
new file mode 100644
index 00000000..a4fbf0bd
--- /dev/null
+++ b/src/Ryujinx.Common/SystemInterop/WindowsMultimediaTimerResolution.cs
@@ -0,0 +1,114 @@
+using Ryujinx.Common.Logging;
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Common.SystemInterop
+{
+ /// <summary>
+ /// Handle Windows Multimedia timer resolution.
+ /// </summary>
+ [SupportedOSPlatform("windows")]
+ public partial class WindowsMultimediaTimerResolution : IDisposable
+ {
+ [StructLayout(LayoutKind.Sequential)]
+ public struct TimeCaps
+ {
+ public uint wPeriodMin;
+ public uint wPeriodMax;
+ };
+
+ [LibraryImport("winmm.dll", EntryPoint = "timeGetDevCaps", SetLastError = true)]
+ private static partial uint TimeGetDevCaps(ref TimeCaps timeCaps, uint sizeTimeCaps);
+
+ [LibraryImport("winmm.dll", EntryPoint = "timeBeginPeriod")]
+ private static partial uint TimeBeginPeriod(uint uMilliseconds);
+
+ [LibraryImport("winmm.dll", EntryPoint = "timeEndPeriod")]
+ private static partial uint TimeEndPeriod(uint uMilliseconds);
+
+ private uint _targetResolutionInMilliseconds;
+ private bool _isActive;
+
+ /// <summary>
+ /// Create a new <see cref="WindowsMultimediaTimerResolution"/> and activate the given resolution.
+ /// </summary>
+ /// <param name="targetResolutionInMilliseconds"></param>
+ public WindowsMultimediaTimerResolution(uint targetResolutionInMilliseconds)
+ {
+ _targetResolutionInMilliseconds = targetResolutionInMilliseconds;
+
+ EnsureResolutionSupport();
+ Activate();
+ }
+
+ private void EnsureResolutionSupport()
+ {
+ TimeCaps timeCaps = default;
+
+ uint result = TimeGetDevCaps(ref timeCaps, (uint)Unsafe.SizeOf<TimeCaps>());
+
+ if (result != 0)
+ {
+ Logger.Notice.Print(LogClass.Application, $"timeGetDevCaps failed with result: {result}");
+ }
+ else
+ {
+ uint supportedTargetResolutionInMilliseconds = Math.Min(Math.Max(timeCaps.wPeriodMin, _targetResolutionInMilliseconds), timeCaps.wPeriodMax);
+
+ if (supportedTargetResolutionInMilliseconds != _targetResolutionInMilliseconds)
+ {
+ Logger.Notice.Print(LogClass.Application, $"Target resolution isn't supported by OS, using closest resolution: {supportedTargetResolutionInMilliseconds}ms");
+
+ _targetResolutionInMilliseconds = supportedTargetResolutionInMilliseconds;
+ }
+ }
+ }
+
+ private void Activate()
+ {
+ uint result = TimeBeginPeriod(_targetResolutionInMilliseconds);
+
+ if (result != 0)
+ {
+ Logger.Notice.Print(LogClass.Application, $"timeBeginPeriod failed with result: {result}");
+ }
+ else
+ {
+ _isActive = true;
+ }
+ }
+
+ private void Disable()
+ {
+ if (_isActive)
+ {
+ uint result = TimeEndPeriod(_targetResolutionInMilliseconds);
+
+ if (result != 0)
+ {
+ Logger.Notice.Print(LogClass.Application, $"timeEndPeriod failed with result: {result}");
+ }
+ else
+ {
+ _isActive = false;
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ Disable();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Utilities/BitUtils.cs b/src/Ryujinx.Common/Utilities/BitUtils.cs
new file mode 100644
index 00000000..f885ce84
--- /dev/null
+++ b/src/Ryujinx.Common/Utilities/BitUtils.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Numerics;
+
+namespace Ryujinx.Common
+{
+ public static class BitUtils
+ {
+ public static T AlignUp<T>(T value, T size)
+ where T : IBinaryInteger<T>
+ {
+ return (value + (size - T.One)) & -size;
+ }
+
+ public static T AlignDown<T>(T value, T size)
+ where T : IBinaryInteger<T>
+ {
+ return value & -size;
+ }
+
+ public static T DivRoundUp<T>(T value, T dividend)
+ where T : IBinaryInteger<T>
+ {
+ return (value + (dividend - T.One)) / dividend;
+ }
+
+ public static int Pow2RoundUp(int value)
+ {
+ value--;
+
+ value |= (value >> 1);
+ value |= (value >> 2);
+ value |= (value >> 4);
+ value |= (value >> 8);
+ value |= (value >> 16);
+
+ return ++value;
+ }
+
+ public static int Pow2RoundDown(int value)
+ {
+ return BitOperations.IsPow2(value) ? value : Pow2RoundUp(value) >> 1;
+ }
+
+ public static long ReverseBits64(long value)
+ {
+ return (long)ReverseBits64((ulong)value);
+ }
+
+ private static ulong ReverseBits64(ulong value)
+ {
+ value = ((value & 0xaaaaaaaaaaaaaaaa) >> 1 ) | ((value & 0x5555555555555555) << 1 );
+ value = ((value & 0xcccccccccccccccc) >> 2 ) | ((value & 0x3333333333333333) << 2 );
+ value = ((value & 0xf0f0f0f0f0f0f0f0) >> 4 ) | ((value & 0x0f0f0f0f0f0f0f0f) << 4 );
+ value = ((value & 0xff00ff00ff00ff00) >> 8 ) | ((value & 0x00ff00ff00ff00ff) << 8 );
+ value = ((value & 0xffff0000ffff0000) >> 16) | ((value & 0x0000ffff0000ffff) << 16);
+
+ return (value >> 32) | (value << 32);
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Utilities/BitfieldExtensions.cs b/src/Ryujinx.Common/Utilities/BitfieldExtensions.cs
new file mode 100644
index 00000000..ca429944
--- /dev/null
+++ b/src/Ryujinx.Common/Utilities/BitfieldExtensions.cs
@@ -0,0 +1,57 @@
+using System.Numerics;
+using System.Runtime.CompilerServices;
+
+namespace Ryujinx.Common.Utilities
+{
+ public static class BitfieldExtensions
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool Extract<T>(this T value, int lsb) where T : IBinaryInteger<T>
+ {
+ int bitSize = Unsafe.SizeOf<T>() * 8;
+ lsb &= bitSize - 1;
+
+ return !T.IsZero((value >>> lsb) & T.One);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static T Extract<T>(this T value, int lsb, int length) where T : IBinaryInteger<T>
+ {
+ int bitSize = Unsafe.SizeOf<T>() * 8;
+ lsb &= bitSize - 1;
+
+ return (value >>> lsb) & (~T.Zero >>> (bitSize - length));
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static T ExtractSx<T>(this T value, int lsb, int length) where T : IBinaryInteger<T>
+ {
+ int bitSize = Unsafe.SizeOf<T>() * 8;
+ int shift = lsb & (bitSize - 1);
+
+ return (value << (bitSize - (shift + length))) >> (bitSize - length);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static T Insert<T>(this T value, int lsb, bool toInsert) where T : IBinaryInteger<T>
+ {
+ int bitSize = Unsafe.SizeOf<T>() * 8;
+ lsb &= bitSize - 1;
+
+ T mask = T.One << lsb;
+
+ return (value & ~mask) | (toInsert ? mask : T.Zero);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static T Insert<T>(this T value, int lsb, int length, T toInsert) where T : IBinaryInteger<T>
+ {
+ int bitSize = Unsafe.SizeOf<T>() * 8;
+ lsb &= bitSize - 1;
+
+ T mask = (~T.Zero >>> (bitSize - length)) << lsb;
+
+ return (value & ~mask) | ((toInsert << lsb) & mask);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Utilities/Buffers.cs b/src/Ryujinx.Common/Utilities/Buffers.cs
new file mode 100644
index 00000000..d614bfc4
--- /dev/null
+++ b/src/Ryujinx.Common/Utilities/Buffers.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Common.Utilities
+{
+ [DebuggerDisplay("{ToString()}")]
+ [StructLayout(LayoutKind.Sequential, Size = 16)]
+ public struct Buffer16
+ {
+ [DebuggerBrowsable(DebuggerBrowsableState.Never)] private ulong _dummy0;
+ [DebuggerBrowsable(DebuggerBrowsableState.Never)] private ulong _dummy1;
+
+ public byte this[int i]
+ {
+ get => Bytes[i];
+ set => Bytes[i] = value;
+ }
+
+ public Span<byte> Bytes => SpanHelpers.AsByteSpan(ref this);
+
+ // Prevent a defensive copy by changing the read-only in reference to a reference with Unsafe.AsRef()
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static implicit operator Span<byte>(in Buffer16 value)
+ {
+ return SpanHelpers.AsByteSpan(ref Unsafe.AsRef(in value));
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static implicit operator ReadOnlySpan<byte>(in Buffer16 value)
+ {
+ return SpanHelpers.AsReadOnlyByteSpan(ref Unsafe.AsRef(in value));
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ref T As<T>() where T : unmanaged
+ {
+ if (Unsafe.SizeOf<T>() > (uint)Unsafe.SizeOf<Buffer16>())
+ {
+ throw new ArgumentException();
+ }
+
+ return ref MemoryMarshal.GetReference(AsSpan<T>());
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Span<T> AsSpan<T>() where T : unmanaged
+ {
+ return SpanHelpers.AsSpan<Buffer16, T>(ref this);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public readonly ReadOnlySpan<T> AsReadOnlySpan<T>() where T : unmanaged
+ {
+ return SpanHelpers.AsReadOnlySpan<Buffer16, T>(ref Unsafe.AsRef(in this));
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Utilities/CommonJsonContext.cs b/src/Ryujinx.Common/Utilities/CommonJsonContext.cs
new file mode 100644
index 00000000..d7b3f78c
--- /dev/null
+++ b/src/Ryujinx.Common/Utilities/CommonJsonContext.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Utilities
+{
+ [JsonSerializable(typeof(string[]), TypeInfoPropertyName = "StringArray")]
+ [JsonSerializable(typeof(Dictionary<string, string>), TypeInfoPropertyName = "StringDictionary")]
+ public partial class CommonJsonContext : JsonSerializerContext
+ {
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Utilities/EmbeddedResources.cs b/src/Ryujinx.Common/Utilities/EmbeddedResources.cs
new file mode 100644
index 00000000..22b55f16
--- /dev/null
+++ b/src/Ryujinx.Common/Utilities/EmbeddedResources.cs
@@ -0,0 +1,148 @@
+using Ryujinx.Common.Memory;
+using Ryujinx.Common.Utilities;
+using System;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Threading.Tasks;
+
+namespace Ryujinx.Common
+{
+ public static class EmbeddedResources
+ {
+ private readonly static Assembly ResourceAssembly;
+
+ static EmbeddedResources()
+ {
+ ResourceAssembly = Assembly.GetAssembly(typeof(EmbeddedResources));
+ }
+
+ public static byte[] Read(string filename)
+ {
+ var (assembly, path) = ResolveManifestPath(filename);
+
+ return Read(assembly, path);
+ }
+
+ public static Task<byte[]> ReadAsync(string filename)
+ {
+ var (assembly, path) = ResolveManifestPath(filename);
+
+ return ReadAsync(assembly, path);
+ }
+
+ public static byte[] Read(Assembly assembly, string filename)
+ {
+ using (var stream = GetStream(assembly, filename))
+ {
+ if (stream == null)
+ {
+ return null;
+ }
+
+ return StreamUtils.StreamToBytes(stream);
+ }
+ }
+
+ public async static Task<byte[]> ReadAsync(Assembly assembly, string filename)
+ {
+ using (var stream = GetStream(assembly, filename))
+ {
+ if (stream == null)
+ {
+ return null;
+ }
+
+ return await StreamUtils.StreamToBytesAsync(stream);
+ }
+ }
+
+ public static string ReadAllText(string filename)
+ {
+ var (assembly, path) = ResolveManifestPath(filename);
+
+ return ReadAllText(assembly, path);
+ }
+
+ public static Task<string> ReadAllTextAsync(string filename)
+ {
+ var (assembly, path) = ResolveManifestPath(filename);
+
+ return ReadAllTextAsync(assembly, path);
+ }
+
+ public static string ReadAllText(Assembly assembly, string filename)
+ {
+ using (var stream = GetStream(assembly, filename))
+ {
+ if (stream == null)
+ {
+ return null;
+ }
+
+ using (var reader = new StreamReader(stream))
+ {
+ return reader.ReadToEnd();
+ }
+ }
+ }
+
+ public async static Task<string> ReadAllTextAsync(Assembly assembly, string filename)
+ {
+ using (var stream = GetStream(assembly, filename))
+ {
+ if (stream == null)
+ {
+ return null;
+ }
+
+ using (var reader = new StreamReader(stream))
+ {
+ return await reader.ReadToEndAsync();
+ }
+ }
+ }
+
+ public static Stream GetStream(string filename)
+ {
+ var (assembly, path) = ResolveManifestPath(filename);
+
+ return GetStream(assembly, path);
+ }
+
+ public static Stream GetStream(Assembly assembly, string filename)
+ {
+ var namespace_ = assembly.GetName().Name;
+ var manifestUri = namespace_ + "." + filename.Replace('/', '.');
+
+ var stream = assembly.GetManifestResourceStream(manifestUri);
+
+ return stream;
+ }
+
+ public static string[] GetAllAvailableResources(string path, string ext = "")
+ {
+ return ResolveManifestPath(path).Item1.GetManifestResourceNames()
+ .Where(r => r.EndsWith(ext))
+ .ToArray();
+ }
+
+ private static (Assembly, string) ResolveManifestPath(string filename)
+ {
+ var segments = filename.Split('/', 2, StringSplitOptions.RemoveEmptyEntries);
+
+ if (segments.Length >= 2)
+ {
+ foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
+ {
+ if (assembly.GetName().Name == segments[0])
+ {
+ return (assembly, segments[1]);
+ }
+ }
+ }
+
+ return (ResourceAssembly, filename);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Utilities/HexUtils.cs b/src/Ryujinx.Common/Utilities/HexUtils.cs
new file mode 100644
index 00000000..63587cea
--- /dev/null
+++ b/src/Ryujinx.Common/Utilities/HexUtils.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Text;
+
+namespace Ryujinx.Common
+{
+ public static class HexUtils
+ {
+ private static readonly char[] HexChars = "0123456789ABCDEF".ToCharArray();
+
+ private const int HexTableColumnWidth = 8;
+ private const int HexTableColumnSpace = 3;
+
+ // Modified for Ryujinx
+ // Original by Pascal Ganaye - CPOL License
+ // https://www.codeproject.com/Articles/36747/Quick-and-Dirty-HexDump-of-a-Byte-Array
+ public static string HexTable(byte[] bytes, int bytesPerLine = 16)
+ {
+ if (bytes == null)
+ {
+ return "<null>";
+ }
+
+ int bytesLength = bytes.Length;
+
+ int firstHexColumn =
+ HexTableColumnWidth
+ + HexTableColumnSpace;
+
+ int firstCharColumn = firstHexColumn
+ + bytesPerLine * HexTableColumnSpace
+ + (bytesPerLine - 1) / HexTableColumnWidth
+ + 2;
+
+ int lineLength = firstCharColumn
+ + bytesPerLine
+ + Environment.NewLine.Length;
+
+ char[] line = (new String(' ', lineLength - Environment.NewLine.Length) + Environment.NewLine).ToCharArray();
+
+ int expectedLines = (bytesLength + bytesPerLine - 1) / bytesPerLine;
+
+ StringBuilder result = new StringBuilder(expectedLines * lineLength);
+
+ for (int i = 0; i < bytesLength; i += bytesPerLine)
+ {
+ line[0] = HexChars[(i >> 28) & 0xF];
+ line[1] = HexChars[(i >> 24) & 0xF];
+ line[2] = HexChars[(i >> 20) & 0xF];
+ line[3] = HexChars[(i >> 16) & 0xF];
+ line[4] = HexChars[(i >> 12) & 0xF];
+ line[5] = HexChars[(i >> 8) & 0xF];
+ line[6] = HexChars[(i >> 4) & 0xF];
+ line[7] = HexChars[(i >> 0) & 0xF];
+
+ int hexColumn = firstHexColumn;
+ int charColumn = firstCharColumn;
+
+ for (int j = 0; j < bytesPerLine; j++)
+ {
+ if (j > 0 && (j & 7) == 0)
+ {
+ hexColumn++;
+ }
+
+ if (i + j >= bytesLength)
+ {
+ line[hexColumn] = ' ';
+ line[hexColumn + 1] = ' ';
+ line[charColumn] = ' ';
+ }
+ else
+ {
+ byte b = bytes[i + j];
+
+ line[hexColumn] = HexChars[(b >> 4) & 0xF];
+ line[hexColumn + 1] = HexChars[b & 0xF];
+ line[charColumn] = (b < 32 ? '·' : (char)b);
+ }
+
+ hexColumn += 3;
+ charColumn++;
+ }
+
+ result.Append(line);
+ }
+
+ return result.ToString();
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Utilities/JsonHelper.cs b/src/Ryujinx.Common/Utilities/JsonHelper.cs
new file mode 100644
index 00000000..9a2d6f18
--- /dev/null
+++ b/src/Ryujinx.Common/Utilities/JsonHelper.cs
@@ -0,0 +1,98 @@
+using System.IO;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization.Metadata;
+
+namespace Ryujinx.Common.Utilities
+{
+ public class JsonHelper
+ {
+ private static readonly JsonNamingPolicy SnakeCasePolicy = new SnakeCaseNamingPolicy();
+ private const int DefaultFileWriteBufferSize = 4096;
+
+ /// <summary>
+ /// Creates new serializer options with default settings.
+ /// </summary>
+ /// <remarks>
+ /// It is REQUIRED for you to save returned options statically or as a part of static serializer context
+ /// in order to avoid performance issues. You can safely modify returned options for your case before storing.
+ /// </remarks>
+ public static JsonSerializerOptions GetDefaultSerializerOptions(bool indented = true)
+ {
+ JsonSerializerOptions options = new()
+ {
+ DictionaryKeyPolicy = SnakeCasePolicy,
+ PropertyNamingPolicy = SnakeCasePolicy,
+ WriteIndented = indented,
+ AllowTrailingCommas = true,
+ ReadCommentHandling = JsonCommentHandling.Skip
+ };
+
+ return options;
+ }
+
+ public static string Serialize<T>(T value, JsonTypeInfo<T> typeInfo)
+ {
+ return JsonSerializer.Serialize(value, typeInfo);
+ }
+
+ public static T Deserialize<T>(string value, JsonTypeInfo<T> typeInfo)
+ {
+ return JsonSerializer.Deserialize(value, typeInfo);
+ }
+
+ public static void SerializeToFile<T>(string filePath, T value, JsonTypeInfo<T> typeInfo)
+ {
+ using FileStream file = File.Create(filePath, DefaultFileWriteBufferSize, FileOptions.WriteThrough);
+ JsonSerializer.Serialize(file, value, typeInfo);
+ }
+
+ public static T DeserializeFromFile<T>(string filePath, JsonTypeInfo<T> typeInfo)
+ {
+ using FileStream file = File.OpenRead(filePath);
+ return JsonSerializer.Deserialize(file, typeInfo);
+ }
+
+ public static void SerializeToStream<T>(Stream stream, T value, JsonTypeInfo<T> typeInfo)
+ {
+ JsonSerializer.Serialize(stream, value, typeInfo);
+ }
+
+ private class SnakeCaseNamingPolicy : JsonNamingPolicy
+ {
+ public override string ConvertName(string name)
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ return name;
+ }
+
+ StringBuilder builder = new();
+
+ for (int i = 0; i < name.Length; i++)
+ {
+ char c = name[i];
+
+ if (char.IsUpper(c))
+ {
+ if (i == 0 || char.IsUpper(name[i - 1]))
+ {
+ builder.Append(char.ToLowerInvariant(c));
+ }
+ else
+ {
+ builder.Append('_');
+ builder.Append(char.ToLowerInvariant(c));
+ }
+ }
+ else
+ {
+ builder.Append(c);
+ }
+ }
+
+ return builder.ToString();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Utilities/MessagePackObjectFormatter.cs b/src/Ryujinx.Common/Utilities/MessagePackObjectFormatter.cs
new file mode 100644
index 00000000..3714bec0
--- /dev/null
+++ b/src/Ryujinx.Common/Utilities/MessagePackObjectFormatter.cs
@@ -0,0 +1,302 @@
+using MsgPack;
+using System;
+using System.Text;
+
+namespace Ryujinx.Common.Utilities
+{
+ public static class MessagePackObjectFormatter
+ {
+ public static string ToString(this MessagePackObject obj, bool pretty)
+ {
+ if (pretty)
+ {
+ return Format(obj);
+ }
+ else
+ {
+ return obj.ToString();
+ }
+ }
+
+ public static string Format(MessagePackObject obj)
+ {
+ var builder = new IndentedStringBuilder();
+
+ FormatMsgPackObj(obj, builder);
+
+ return builder.ToString();
+ }
+
+ private static void FormatMsgPackObj(MessagePackObject obj, IndentedStringBuilder builder)
+ {
+ if (obj.IsMap || obj.IsDictionary)
+ {
+ FormatMsgPackMap(obj, builder);
+ }
+ else if (obj.IsArray || obj.IsList)
+ {
+ FormatMsgPackArray(obj, builder);
+ }
+ else if (obj.IsNil)
+ {
+ builder.Append("null");
+ }
+ else
+ {
+ var literal = obj.ToObject();
+
+ if (literal is String)
+ {
+ builder.AppendQuotedString(obj.AsStringUtf16());
+ }
+ else if (literal is byte[] byteArray)
+ {
+ FormatByteArray(byteArray, builder);
+ }
+ else if (literal is MessagePackExtendedTypeObject extObject)
+ {
+ builder.Append('{');
+
+ // Indent
+ builder.IncreaseIndent()
+ .AppendLine();
+
+ // Print TypeCode field
+ builder.AppendQuotedString("TypeCode")
+ .Append(": ")
+ .Append(extObject.TypeCode)
+ .AppendLine(",");
+
+ // Print Value field
+ builder.AppendQuotedString("Value")
+ .Append(": ");
+
+ FormatByteArrayAsString(extObject.GetBody(), builder, true);
+
+ // Unindent
+ builder.DecreaseIndent()
+ .AppendLine();
+
+ builder.Append('}');
+ }
+ else
+ {
+ builder.Append(literal);
+ }
+ }
+ }
+
+ private static void FormatByteArray(byte[] arr, IndentedStringBuilder builder)
+ {
+ builder.Append("[ ");
+
+ foreach (var b in arr)
+ {
+ builder.Append("0x");
+ builder.Append(ToHexChar(b >> 4));
+ builder.Append(ToHexChar(b & 0xF));
+ builder.Append(", ");
+ }
+
+ // Remove trailing comma
+ builder.Remove(builder.Length - 2, 2);
+
+ builder.Append(" ]");
+ }
+
+ private static void FormatByteArrayAsString(byte[] arr, IndentedStringBuilder builder, bool withPrefix)
+ {
+ builder.Append('"');
+
+ if (withPrefix)
+ {
+ builder.Append("0x");
+ }
+
+ foreach (var b in arr)
+ {
+ builder.Append(ToHexChar(b >> 4));
+ builder.Append(ToHexChar(b & 0xF));
+ }
+
+ builder.Append('"');
+ }
+
+ private static void FormatMsgPackMap(MessagePackObject obj, IndentedStringBuilder builder)
+ {
+ var map = obj.AsDictionary();
+
+ builder.Append('{');
+
+ // Indent
+ builder.IncreaseIndent()
+ .AppendLine();
+
+ foreach (var item in map)
+ {
+ FormatMsgPackObj(item.Key, builder);
+
+ builder.Append(": ");
+
+ FormatMsgPackObj(item.Value, builder);
+
+ builder.AppendLine(",");
+ }
+
+ // Remove the trailing new line and comma
+ builder.TrimLastLine()
+ .Remove(builder.Length - 1, 1);
+
+ // Unindent
+ builder.DecreaseIndent()
+ .AppendLine();
+
+ builder.Append('}');
+ }
+
+ private static void FormatMsgPackArray(MessagePackObject obj, IndentedStringBuilder builder)
+ {
+ var arr = obj.AsList();
+
+ builder.Append("[ ");
+
+ foreach (var item in arr)
+ {
+ FormatMsgPackObj(item, builder);
+
+ builder.Append(", ");
+ }
+
+ // Remove trailing comma
+ builder.Remove(builder.Length - 2, 2);
+
+ builder.Append(" ]");
+ }
+
+ private static char ToHexChar(int b)
+ {
+ if (b < 10)
+ {
+ return unchecked((char)('0' + b));
+ }
+ else
+ {
+ return unchecked((char)('A' + (b - 10)));
+ }
+ }
+
+ internal class IndentedStringBuilder
+ {
+ const string DefaultIndent = " ";
+
+ private int _indentCount = 0;
+ private int _newLineIndex = 0;
+ private StringBuilder _builder;
+
+ public string IndentString { get; set; } = DefaultIndent;
+
+ public IndentedStringBuilder(StringBuilder builder)
+ {
+ _builder = builder;
+ }
+
+ public IndentedStringBuilder()
+ : this(new StringBuilder())
+ { }
+
+ public IndentedStringBuilder(string str)
+ : this(new StringBuilder(str))
+ { }
+
+ public IndentedStringBuilder(int length)
+ : this(new StringBuilder(length))
+ { }
+
+ public int Length { get => _builder.Length; }
+
+ public IndentedStringBuilder IncreaseIndent()
+ {
+ _indentCount++;
+
+ return this;
+ }
+
+ public IndentedStringBuilder DecreaseIndent()
+ {
+ _indentCount--;
+
+ return this;
+ }
+
+ public IndentedStringBuilder Append(char value)
+ {
+ _builder.Append(value);
+
+ return this;
+ }
+
+ public IndentedStringBuilder Append(string value)
+ {
+ _builder.Append(value);
+
+ return this;
+ }
+
+ public IndentedStringBuilder Append(object value)
+ {
+ this.Append(value.ToString());
+
+ return this;
+ }
+
+ public IndentedStringBuilder AppendQuotedString(string value)
+ {
+ _builder.Append('"');
+ _builder.Append(value);
+ _builder.Append('"');
+
+ return this;
+ }
+
+ public IndentedStringBuilder AppendLine()
+ {
+ _newLineIndex = _builder.Length;
+
+ _builder.AppendLine();
+
+ for (int i = 0; i < _indentCount; i++)
+ _builder.Append(IndentString);
+
+ return this;
+ }
+
+ public IndentedStringBuilder AppendLine(string value)
+ {
+ _builder.Append(value);
+
+ this.AppendLine();
+
+ return this;
+ }
+
+ public IndentedStringBuilder TrimLastLine()
+ {
+ _builder.Remove(_newLineIndex, _builder.Length - _newLineIndex);
+
+ return this;
+ }
+
+ public IndentedStringBuilder Remove(int startIndex, int length)
+ {
+ _builder.Remove(startIndex, length);
+
+ return this;
+ }
+
+ public override string ToString()
+ {
+ return _builder.ToString();
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Utilities/NetworkHelpers.cs b/src/Ryujinx.Common/Utilities/NetworkHelpers.cs
new file mode 100644
index 00000000..b2f6f45d
--- /dev/null
+++ b/src/Ryujinx.Common/Utilities/NetworkHelpers.cs
@@ -0,0 +1,66 @@
+using System.Net.NetworkInformation;
+
+namespace Ryujinx.Common.Utilities
+{
+ public static class NetworkHelpers
+ {
+ private static (IPInterfaceProperties, UnicastIPAddressInformation) GetLocalInterface(NetworkInterface adapter, bool isPreferred)
+ {
+ IPInterfaceProperties properties = adapter.GetIPProperties();
+
+ if (isPreferred || (properties.GatewayAddresses.Count > 0 && properties.DnsAddresses.Count > 0))
+ {
+ foreach (UnicastIPAddressInformation info in properties.UnicastAddresses)
+ {
+ // Only accept an IPv4 address
+ if (info.Address.GetAddressBytes().Length == 4)
+ {
+ return (properties, info);
+ }
+ }
+ }
+
+ return (null, null);
+ }
+
+ public static (IPInterfaceProperties, UnicastIPAddressInformation) GetLocalInterface(string lanInterfaceId = "0")
+ {
+ if (!NetworkInterface.GetIsNetworkAvailable())
+ {
+ return (null, null);
+ }
+
+ IPInterfaceProperties targetProperties = null;
+ UnicastIPAddressInformation targetAddressInfo = null;
+
+ NetworkInterface[] interfaces = NetworkInterface.GetAllNetworkInterfaces();
+
+ string guid = lanInterfaceId;
+ bool hasPreference = guid != "0";
+
+ foreach (NetworkInterface adapter in interfaces)
+ {
+ bool isPreferred = adapter.Id == guid;
+
+ // Ignore loopback and non IPv4 capable interface.
+ if (isPreferred || (targetProperties == null && adapter.NetworkInterfaceType != NetworkInterfaceType.Loopback && adapter.Supports(NetworkInterfaceComponent.IPv4)))
+ {
+ (IPInterfaceProperties properties, UnicastIPAddressInformation info) = GetLocalInterface(adapter, isPreferred);
+
+ if (properties != null)
+ {
+ targetProperties = properties;
+ targetAddressInfo = info;
+
+ if (isPreferred || !hasPreference)
+ {
+ break;
+ }
+ }
+ }
+ }
+
+ return (targetProperties, targetAddressInfo);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Utilities/SpanHelpers.cs b/src/Ryujinx.Common/Utilities/SpanHelpers.cs
new file mode 100644
index 00000000..4765eab3
--- /dev/null
+++ b/src/Ryujinx.Common/Utilities/SpanHelpers.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Common.Utilities
+{
+ public static class SpanHelpers
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Span<T> CreateSpan<T>(scoped ref T reference, int length)
+ {
+ return MemoryMarshal.CreateSpan(ref reference, length);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Span<T> AsSpan<T>(scoped ref T reference) where T : unmanaged
+ {
+ return CreateSpan(ref reference, 1);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Span<TSpan> AsSpan<TStruct, TSpan>(scoped ref TStruct reference)
+ where TStruct : unmanaged where TSpan : unmanaged
+ {
+ return CreateSpan(ref Unsafe.As<TStruct, TSpan>(ref reference),
+ Unsafe.SizeOf<TStruct>() / Unsafe.SizeOf<TSpan>());
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Span<byte> AsByteSpan<T>(scoped ref T reference) where T : unmanaged
+ {
+ return CreateSpan(ref Unsafe.As<T, byte>(ref reference), Unsafe.SizeOf<T>());
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ReadOnlySpan<T> CreateReadOnlySpan<T>(scoped ref T reference, int length)
+ {
+ return MemoryMarshal.CreateReadOnlySpan(ref reference, length);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ReadOnlySpan<T> AsReadOnlySpan<T>(scoped ref T reference) where T : unmanaged
+ {
+ return CreateReadOnlySpan(ref reference, 1);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ReadOnlySpan<TSpan> AsReadOnlySpan<TStruct, TSpan>(scoped ref TStruct reference)
+ where TStruct : unmanaged where TSpan : unmanaged
+ {
+ return CreateReadOnlySpan(ref Unsafe.As<TStruct, TSpan>(ref reference),
+ Unsafe.SizeOf<TStruct>() / Unsafe.SizeOf<TSpan>());
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ReadOnlySpan<byte> AsReadOnlyByteSpan<T>(scoped ref T reference) where T : unmanaged
+ {
+ return CreateReadOnlySpan(ref Unsafe.As<T, byte>(ref reference), Unsafe.SizeOf<T>());
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Utilities/StreamUtils.cs b/src/Ryujinx.Common/Utilities/StreamUtils.cs
new file mode 100644
index 00000000..da97188d
--- /dev/null
+++ b/src/Ryujinx.Common/Utilities/StreamUtils.cs
@@ -0,0 +1,31 @@
+using Microsoft.IO;
+using Ryujinx.Common.Memory;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Ryujinx.Common.Utilities
+{
+ public static class StreamUtils
+ {
+ public static byte[] StreamToBytes(Stream input)
+ {
+ using (MemoryStream stream = MemoryStreamManager.Shared.GetStream())
+ {
+ input.CopyTo(stream);
+
+ return stream.ToArray();
+ }
+ }
+
+ public static async Task<byte[]> StreamToBytesAsync(Stream input, CancellationToken cancellationToken = default)
+ {
+ using (MemoryStream stream = MemoryStreamManager.Shared.GetStream())
+ {
+ await input.CopyToAsync(stream, cancellationToken);
+
+ return stream.ToArray();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Utilities/TypedStringEnumConverter.cs b/src/Ryujinx.Common/Utilities/TypedStringEnumConverter.cs
new file mode 100644
index 00000000..c0127dc4
--- /dev/null
+++ b/src/Ryujinx.Common/Utilities/TypedStringEnumConverter.cs
@@ -0,0 +1,34 @@
+#nullable enable
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Utilities
+{
+ /// <summary>
+ /// Specifies that value of <see cref="TEnum"/> will be serialized as string in JSONs
+ /// </summary>
+ /// <remarks>
+ /// Trimming friendly alternative to <see cref="JsonStringEnumConverter"/>.
+ /// Get rid of this converter if dotnet supports similar functionality out of the box.
+ /// </remarks>
+ /// <typeparam name="TEnum">Type of enum to serialize</typeparam>
+ public sealed class TypedStringEnumConverter<TEnum> : JsonConverter<TEnum> where TEnum : struct, Enum
+ {
+ public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var enumValue = reader.GetString();
+ if (string.IsNullOrEmpty(enumValue))
+ {
+ return default;
+ }
+
+ return Enum.Parse<TEnum>(enumValue);
+ }
+
+ public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options)
+ {
+ writer.WriteStringValue(value.ToString());
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Utilities/UInt128Utils.cs b/src/Ryujinx.Common/Utilities/UInt128Utils.cs
new file mode 100644
index 00000000..af8521b4
--- /dev/null
+++ b/src/Ryujinx.Common/Utilities/UInt128Utils.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Globalization;
+
+namespace Ryujinx.Common.Utilities
+{
+ public static class UInt128Utils
+ {
+ public static UInt128 FromHex(string hex)
+ {
+ return new UInt128(ulong.Parse(hex.AsSpan(0, 16), NumberStyles.HexNumber), ulong.Parse(hex.AsSpan(16), NumberStyles.HexNumber));
+ }
+
+ public static UInt128 CreateRandom()
+ {
+ return new UInt128((ulong)Random.Shared.NextInt64(), (ulong)Random.Shared.NextInt64());
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/XXHash128.cs b/src/Ryujinx.Common/XXHash128.cs
new file mode 100644
index 00000000..edbc652f
--- /dev/null
+++ b/src/Ryujinx.Common/XXHash128.cs
@@ -0,0 +1,537 @@
+using System;
+using System.Buffers.Binary;
+using System.Diagnostics;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.Intrinsics;
+using System.Runtime.Intrinsics.X86;
+
+namespace Ryujinx.Common
+{
+ public static class XXHash128
+ {
+ private const int StripeLen = 64;
+ private const int AccNb = StripeLen / sizeof(ulong);
+ private const int SecretConsumeRate = 8;
+ private const int SecretLastAccStart = 7;
+ private const int SecretMergeAccsStart = 11;
+ private const int SecretSizeMin = 136;
+ private const int MidSizeStartOffset = 3;
+ private const int MidSizeLastOffset = 17;
+
+ private const uint Prime32_1 = 0x9E3779B1U;
+ private const uint Prime32_2 = 0x85EBCA77U;
+ private const uint Prime32_3 = 0xC2B2AE3DU;
+ private const uint Prime32_4 = 0x27D4EB2FU;
+ private const uint Prime32_5 = 0x165667B1U;
+
+ private const ulong Prime64_1 = 0x9E3779B185EBCA87UL;
+ private const ulong Prime64_2 = 0xC2B2AE3D27D4EB4FUL;
+ private const ulong Prime64_3 = 0x165667B19E3779F9UL;
+ private const ulong Prime64_4 = 0x85EBCA77C2B2AE63UL;
+ private const ulong Prime64_5 = 0x27D4EB2F165667C5UL;
+
+ private static readonly ulong[] Xxh3InitAcc = new ulong[]
+ {
+ Prime32_3,
+ Prime64_1,
+ Prime64_2,
+ Prime64_3,
+ Prime64_4,
+ Prime32_2,
+ Prime64_5,
+ Prime32_1
+ };
+
+ private static ReadOnlySpan<byte> Xxh3KSecret => new byte[]
+ {
+ 0xb8, 0xfe, 0x6c, 0x39, 0x23, 0xa4, 0x4b, 0xbe, 0x7c, 0x01, 0x81, 0x2c, 0xf7, 0x21, 0xad, 0x1c,
+ 0xde, 0xd4, 0x6d, 0xe9, 0x83, 0x90, 0x97, 0xdb, 0x72, 0x40, 0xa4, 0xa4, 0xb7, 0xb3, 0x67, 0x1f,
+ 0xcb, 0x79, 0xe6, 0x4e, 0xcc, 0xc0, 0xe5, 0x78, 0x82, 0x5a, 0xd0, 0x7d, 0xcc, 0xff, 0x72, 0x21,
+ 0xb8, 0x08, 0x46, 0x74, 0xf7, 0x43, 0x24, 0x8e, 0xe0, 0x35, 0x90, 0xe6, 0x81, 0x3a, 0x26, 0x4c,
+ 0x3c, 0x28, 0x52, 0xbb, 0x91, 0xc3, 0x00, 0xcb, 0x88, 0xd0, 0x65, 0x8b, 0x1b, 0x53, 0x2e, 0xa3,
+ 0x71, 0x64, 0x48, 0x97, 0xa2, 0x0d, 0xf9, 0x4e, 0x38, 0x19, 0xef, 0x46, 0xa9, 0xde, 0xac, 0xd8,
+ 0xa8, 0xfa, 0x76, 0x3f, 0xe3, 0x9c, 0x34, 0x3f, 0xf9, 0xdc, 0xbb, 0xc7, 0xc7, 0x0b, 0x4f, 0x1d,
+ 0x8a, 0x51, 0xe0, 0x4b, 0xcd, 0xb4, 0x59, 0x31, 0xc8, 0x9f, 0x7e, 0xc9, 0xd9, 0x78, 0x73, 0x64,
+ 0xea, 0xc5, 0xac, 0x83, 0x34, 0xd3, 0xeb, 0xc3, 0xc5, 0x81, 0xa0, 0xff, 0xfa, 0x13, 0x63, 0xeb,
+ 0x17, 0x0d, 0xdd, 0x51, 0xb7, 0xf0, 0xda, 0x49, 0xd3, 0x16, 0x55, 0x26, 0x29, 0xd4, 0x68, 0x9e,
+ 0x2b, 0x16, 0xbe, 0x58, 0x7d, 0x47, 0xa1, 0xfc, 0x8f, 0xf8, 0xb8, 0xd1, 0x7a, 0xd0, 0x31, 0xce,
+ 0x45, 0xcb, 0x3a, 0x8f, 0x95, 0x16, 0x04, 0x28, 0xaf, 0xd7, 0xfb, 0xca, 0xbb, 0x4b, 0x40, 0x7e
+ };
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static ulong Mult32To64(ulong x, ulong y)
+ {
+ return (ulong)(uint)x * (ulong)(uint)y;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Hash128 Mult64To128(ulong lhs, ulong rhs)
+ {
+ ulong high = Math.BigMul(lhs, rhs, out ulong low);
+ return new Hash128
+ {
+ Low = low,
+ High = high
+ };
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static ulong Mul128Fold64(ulong lhs, ulong rhs)
+ {
+ Hash128 product = Mult64To128(lhs, rhs);
+ return product.Low ^ product.High;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static ulong XorShift64(ulong v64, int shift)
+ {
+ Debug.Assert(0 <= shift && shift < 64);
+ return v64 ^ (v64 >> shift);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static ulong Xxh3Avalanche(ulong h64)
+ {
+ h64 = XorShift64(h64, 37);
+ h64 *= 0x165667919E3779F9UL;
+ h64 = XorShift64(h64, 32);
+ return h64;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static ulong Xxh64Avalanche(ulong h64)
+ {
+ h64 ^= h64 >> 33;
+ h64 *= Prime64_2;
+ h64 ^= h64 >> 29;
+ h64 *= Prime64_3;
+ h64 ^= h64 >> 32;
+ return h64;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private unsafe static void Xxh3Accumulate512(Span<ulong> acc, ReadOnlySpan<byte> input, ReadOnlySpan<byte> secret)
+ {
+ if (Avx2.IsSupported)
+ {
+ fixed (ulong* pAcc = acc)
+ {
+ fixed (byte* pInput = input, pSecret = secret)
+ {
+ Vector256<ulong>* xAcc = (Vector256<ulong>*)pAcc;
+ Vector256<byte>* xInput = (Vector256<byte>*)pInput;
+ Vector256<byte>* xSecret = (Vector256<byte>*)pSecret;
+
+ for (ulong i = 0; i < StripeLen / 32; i++)
+ {
+ Vector256<byte> dataVec = xInput[i];
+ Vector256<byte> keyVec = xSecret[i];
+ Vector256<byte> dataKey = Avx2.Xor(dataVec, keyVec);
+ Vector256<uint> dataKeyLo = Avx2.Shuffle(dataKey.AsUInt32(), 0b00110001);
+ Vector256<ulong> product = Avx2.Multiply(dataKey.AsUInt32(), dataKeyLo);
+ Vector256<uint> dataSwap = Avx2.Shuffle(dataVec.AsUInt32(), 0b01001110);
+ Vector256<ulong> sum = Avx2.Add(xAcc[i], dataSwap.AsUInt64());
+ xAcc[i] = Avx2.Add(product, sum);
+ }
+ }
+ }
+ }
+ else if (Sse2.IsSupported)
+ {
+ fixed (ulong* pAcc = acc)
+ {
+ fixed (byte* pInput = input, pSecret = secret)
+ {
+ Vector128<ulong>* xAcc = (Vector128<ulong>*)pAcc;
+ Vector128<byte>* xInput = (Vector128<byte>*)pInput;
+ Vector128<byte>* xSecret = (Vector128<byte>*)pSecret;
+
+ for (ulong i = 0; i < StripeLen / 16; i++)
+ {
+ Vector128<byte> dataVec = xInput[i];
+ Vector128<byte> keyVec = xSecret[i];
+ Vector128<byte> dataKey = Sse2.Xor(dataVec, keyVec);
+ Vector128<uint> dataKeyLo = Sse2.Shuffle(dataKey.AsUInt32(), 0b00110001);
+ Vector128<ulong> product = Sse2.Multiply(dataKey.AsUInt32(), dataKeyLo);
+ Vector128<uint> dataSwap = Sse2.Shuffle(dataVec.AsUInt32(), 0b01001110);
+ Vector128<ulong> sum = Sse2.Add(xAcc[i], dataSwap.AsUInt64());
+ xAcc[i] = Sse2.Add(product, sum);
+ }
+ }
+ }
+ }
+ else
+ {
+ for (int i = 0; i < AccNb; i++)
+ {
+ ulong dataVal = BinaryPrimitives.ReadUInt64LittleEndian(input.Slice(i * sizeof(ulong)));
+ ulong dataKey = dataVal ^ BinaryPrimitives.ReadUInt64LittleEndian(secret.Slice(i * sizeof(ulong)));
+ acc[i ^ 1] += dataVal;
+ acc[i] += Mult32To64((uint)dataKey, dataKey >> 32);
+ }
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private unsafe static void Xxh3ScrambleAcc(Span<ulong> acc, ReadOnlySpan<byte> secret)
+ {
+ if (Avx2.IsSupported)
+ {
+ fixed (ulong* pAcc = acc)
+ {
+ fixed (byte* pSecret = secret)
+ {
+ Vector256<uint> prime32 = Vector256.Create(Prime32_1);
+ Vector256<ulong>* xAcc = (Vector256<ulong>*)pAcc;
+ Vector256<byte>* xSecret = (Vector256<byte>*)pSecret;
+
+ for (ulong i = 0; i < StripeLen / 32; i++)
+ {
+ Vector256<ulong> accVec = xAcc[i];
+ Vector256<ulong> shifted = Avx2.ShiftRightLogical(accVec, 47);
+ Vector256<ulong> dataVec = Avx2.Xor(accVec, shifted);
+
+ Vector256<byte> keyVec = xSecret[i];
+ Vector256<uint> dataKey = Avx2.Xor(dataVec.AsUInt32(), keyVec.AsUInt32());
+
+ Vector256<uint> dataKeyHi = Avx2.Shuffle(dataKey.AsUInt32(), 0b00110001);
+ Vector256<ulong> prodLo = Avx2.Multiply(dataKey, prime32);
+ Vector256<ulong> prodHi = Avx2.Multiply(dataKeyHi, prime32);
+
+ xAcc[i] = Avx2.Add(prodLo, Avx2.ShiftLeftLogical(prodHi, 32));
+ }
+ }
+ }
+ }
+ else if (Sse2.IsSupported)
+ {
+ fixed (ulong* pAcc = acc)
+ {
+ fixed (byte* pSecret = secret)
+ {
+ Vector128<uint> prime32 = Vector128.Create(Prime32_1);
+ Vector128<ulong>* xAcc = (Vector128<ulong>*)pAcc;
+ Vector128<byte>* xSecret = (Vector128<byte>*)pSecret;
+
+ for (ulong i = 0; i < StripeLen / 16; i++)
+ {
+ Vector128<ulong> accVec = xAcc[i];
+ Vector128<ulong> shifted = Sse2.ShiftRightLogical(accVec, 47);
+ Vector128<ulong> dataVec = Sse2.Xor(accVec, shifted);
+
+ Vector128<byte> keyVec = xSecret[i];
+ Vector128<uint> dataKey = Sse2.Xor(dataVec.AsUInt32(), keyVec.AsUInt32());
+
+ Vector128<uint> dataKeyHi = Sse2.Shuffle(dataKey.AsUInt32(), 0b00110001);
+ Vector128<ulong> prodLo = Sse2.Multiply(dataKey, prime32);
+ Vector128<ulong> prodHi = Sse2.Multiply(dataKeyHi, prime32);
+
+ xAcc[i] = Sse2.Add(prodLo, Sse2.ShiftLeftLogical(prodHi, 32));
+ }
+ }
+ }
+ }
+ else
+ {
+ for (int i = 0; i < AccNb; i++)
+ {
+ ulong key64 = BinaryPrimitives.ReadUInt64LittleEndian(secret.Slice(i * sizeof(ulong)));
+ ulong acc64 = acc[i];
+ acc64 = XorShift64(acc64, 47);
+ acc64 ^= key64;
+ acc64 *= Prime32_1;
+ acc[i] = acc64;
+ }
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void Xxh3Accumulate(Span<ulong> acc, ReadOnlySpan<byte> input, ReadOnlySpan<byte> secret, int nbStripes)
+ {
+ for (int n = 0; n < nbStripes; n++)
+ {
+ ReadOnlySpan<byte> inData = input.Slice(n * StripeLen);
+ Xxh3Accumulate512(acc, inData, secret.Slice(n * SecretConsumeRate));
+ }
+ }
+
+ private static void Xxh3HashLongInternalLoop(Span<ulong> acc, ReadOnlySpan<byte> input, ReadOnlySpan<byte> secret)
+ {
+ int nbStripesPerBlock = (secret.Length - StripeLen) / SecretConsumeRate;
+ int blockLen = StripeLen * nbStripesPerBlock;
+ int nbBlocks = (input.Length - 1) / blockLen;
+
+ Debug.Assert(secret.Length >= SecretSizeMin);
+
+ for (int n = 0; n < nbBlocks; n++)
+ {
+ Xxh3Accumulate(acc, input.Slice(n * blockLen), secret, nbStripesPerBlock);
+ Xxh3ScrambleAcc(acc, secret.Slice(secret.Length - StripeLen));
+ }
+
+ Debug.Assert(input.Length > StripeLen);
+
+ int nbStripes = (input.Length - 1 - (blockLen * nbBlocks)) / StripeLen;
+ Debug.Assert(nbStripes <= (secret.Length / SecretConsumeRate));
+ Xxh3Accumulate(acc, input.Slice(nbBlocks * blockLen), secret, nbStripes);
+
+ ReadOnlySpan<byte> p = input.Slice(input.Length - StripeLen);
+ Xxh3Accumulate512(acc, p, secret.Slice(secret.Length - StripeLen - SecretLastAccStart));
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static ulong Xxh3Mix2Accs(Span<ulong> acc, ReadOnlySpan<byte> secret)
+ {
+ return Mul128Fold64(
+ acc[0] ^ BinaryPrimitives.ReadUInt64LittleEndian(secret),
+ acc[1] ^ BinaryPrimitives.ReadUInt64LittleEndian(secret.Slice(8)));
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static ulong Xxh3MergeAccs(Span<ulong> acc, ReadOnlySpan<byte> secret, ulong start)
+ {
+ ulong result64 = start;
+
+ for (int i = 0; i < 4; i++)
+ {
+ result64 += Xxh3Mix2Accs(acc.Slice(2 * i), secret.Slice(16 * i));
+ }
+
+ return Xxh3Avalanche(result64);
+ }
+
+ [SkipLocalsInit]
+ private static Hash128 Xxh3HashLong128bInternal(ReadOnlySpan<byte> input, ReadOnlySpan<byte> secret)
+ {
+ Span<ulong> acc = stackalloc ulong[AccNb];
+ Xxh3InitAcc.CopyTo(acc);
+
+ Xxh3HashLongInternalLoop(acc, input, secret);
+
+ Debug.Assert(acc.Length == 8);
+ Debug.Assert(secret.Length >= acc.Length * sizeof(ulong) + SecretMergeAccsStart);
+
+ return new Hash128
+ {
+ Low = Xxh3MergeAccs(acc, secret.Slice(SecretMergeAccsStart), (ulong)input.Length * Prime64_1),
+ High = Xxh3MergeAccs(
+ acc,
+ secret.Slice(secret.Length - acc.Length * sizeof(ulong) - SecretMergeAccsStart),
+ ~((ulong)input.Length * Prime64_2))
+ };
+ }
+
+ private static Hash128 Xxh3Len1To3128b(ReadOnlySpan<byte> input, ReadOnlySpan<byte> secret, ulong seed)
+ {
+ Debug.Assert(1 <= input.Length && input.Length <= 3);
+
+ byte c1 = input[0];
+ byte c2 = input[input.Length >> 1];
+ byte c3 = input[^1];
+
+ uint combinedL = ((uint)c1 << 16) | ((uint)c2 << 24) | c3 | ((uint)input.Length << 8);
+ uint combinedH = BitOperations.RotateLeft(BinaryPrimitives.ReverseEndianness(combinedL), 13);
+ ulong bitFlipL = (BinaryPrimitives.ReadUInt32LittleEndian(secret) ^ BinaryPrimitives.ReadUInt32LittleEndian(secret.Slice(4))) + seed;
+ ulong bitFlipH = (BinaryPrimitives.ReadUInt32LittleEndian(secret.Slice(8)) ^ BinaryPrimitives.ReadUInt32LittleEndian(secret.Slice(12))) - seed;
+ ulong keyedLo = combinedL ^ bitFlipL;
+ ulong keyedHi = combinedH ^ bitFlipH;
+
+ return new Hash128
+ {
+ Low = Xxh64Avalanche(keyedLo),
+ High = Xxh64Avalanche(keyedHi)
+ };
+ }
+
+ private static Hash128 Xxh3Len4To8128b(ReadOnlySpan<byte> input, ReadOnlySpan<byte> secret, ulong seed)
+ {
+ Debug.Assert(4 <= input.Length && input.Length <= 8);
+
+ seed ^= BinaryPrimitives.ReverseEndianness((uint)seed) << 32;
+
+ uint inputLo = BinaryPrimitives.ReadUInt32LittleEndian(input);
+ uint inputHi = BinaryPrimitives.ReadUInt32LittleEndian(input.Slice(input.Length - 4));
+ ulong input64 = inputLo + ((ulong)inputHi << 32);
+ ulong bitFlip = (BinaryPrimitives.ReadUInt64LittleEndian(secret.Slice(16)) ^ BinaryPrimitives.ReadUInt64LittleEndian(secret.Slice(24))) + seed;
+ ulong keyed = input64 ^ bitFlip;
+
+ Hash128 m128 = Mult64To128(keyed, Prime64_1 + ((ulong)input.Length << 2));
+
+ m128.High += m128.Low << 1;
+ m128.Low ^= m128.High >> 3;
+
+ m128.Low = XorShift64(m128.Low, 35);
+ m128.Low *= 0x9FB21C651E98DF25UL;
+ m128.Low = XorShift64(m128.Low, 28);
+ m128.High = Xxh3Avalanche(m128.High);
+ return m128;
+ }
+
+ private static Hash128 Xxh3Len9To16128b(ReadOnlySpan<byte> input, ReadOnlySpan<byte> secret, ulong seed)
+ {
+ Debug.Assert(9 <= input.Length && input.Length <= 16);
+
+ ulong bitFlipL = (BinaryPrimitives.ReadUInt64LittleEndian(secret.Slice(32)) ^ BinaryPrimitives.ReadUInt64LittleEndian(secret.Slice(40))) - seed;
+ ulong bitFlipH = (BinaryPrimitives.ReadUInt64LittleEndian(secret.Slice(48)) ^ BinaryPrimitives.ReadUInt64LittleEndian(secret.Slice(56))) + seed;
+ ulong inputLo = BinaryPrimitives.ReadUInt64LittleEndian(input);
+ ulong inputHi = BinaryPrimitives.ReadUInt64LittleEndian(input.Slice(input.Length - 8));
+
+ Hash128 m128 = Mult64To128(inputLo ^ inputHi ^ bitFlipL, Prime64_1);
+ m128.Low += ((ulong)input.Length - 1) << 54;
+ inputHi ^= bitFlipH;
+ m128.High += inputHi + Mult32To64((uint)inputHi, Prime32_2 - 1);
+ m128.Low ^= BinaryPrimitives.ReverseEndianness(m128.High);
+
+ Hash128 h128 = Mult64To128(m128.Low, Prime64_2);
+ h128.High += m128.High * Prime64_2;
+ h128.Low = Xxh3Avalanche(h128.Low);
+ h128.High = Xxh3Avalanche(h128.High);
+ return h128;
+ }
+
+ private static Hash128 Xxh3Len0To16128b(ReadOnlySpan<byte> input, ReadOnlySpan<byte> secret, ulong seed)
+ {
+ Debug.Assert(input.Length <= 16);
+
+ if (input.Length > 8)
+ {
+ return Xxh3Len9To16128b(input, secret, seed);
+ }
+ else if (input.Length >= 4)
+ {
+ return Xxh3Len4To8128b(input, secret, seed);
+ }
+ else if (input.Length != 0)
+ {
+ return Xxh3Len1To3128b(input, secret, seed);
+ }
+ else
+ {
+ Hash128 h128 = new Hash128();
+ ulong bitFlipL = BinaryPrimitives.ReadUInt64LittleEndian(secret.Slice(64)) ^ BinaryPrimitives.ReadUInt64LittleEndian(secret.Slice(72));
+ ulong bitFlipH = BinaryPrimitives.ReadUInt64LittleEndian(secret.Slice(80)) ^ BinaryPrimitives.ReadUInt64LittleEndian(secret.Slice(88));
+ h128.Low = Xxh64Avalanche(seed ^ bitFlipL);
+ h128.High = Xxh64Avalanche(seed ^ bitFlipH);
+ return h128;
+ }
+ }
+
+ private static ulong Xxh3Mix16b(ReadOnlySpan<byte> input, ReadOnlySpan<byte> secret, ulong seed)
+ {
+ ulong inputLo = BinaryPrimitives.ReadUInt64LittleEndian(input);
+ ulong inputHi = BinaryPrimitives.ReadUInt64LittleEndian(input.Slice(8));
+ return Mul128Fold64(
+ inputLo ^ (BinaryPrimitives.ReadUInt64LittleEndian(secret) + seed),
+ inputHi ^ (BinaryPrimitives.ReadUInt64LittleEndian(secret.Slice(8)) - seed));
+ }
+
+ private static Hash128 Xxh128Mix32b(Hash128 acc, ReadOnlySpan<byte> input, ReadOnlySpan<byte> input2, ReadOnlySpan<byte> secret, ulong seed)
+ {
+ acc.Low += Xxh3Mix16b(input, secret, seed);
+ acc.Low ^= BinaryPrimitives.ReadUInt64LittleEndian(input2) + BinaryPrimitives.ReadUInt64LittleEndian(input2.Slice(8));
+ acc.High += Xxh3Mix16b(input2, secret.Slice(16), seed);
+ acc.High ^= BinaryPrimitives.ReadUInt64LittleEndian(input) + BinaryPrimitives.ReadUInt64LittleEndian(input.Slice(8));
+ return acc;
+ }
+
+ private static Hash128 Xxh3Len17To128128b(ReadOnlySpan<byte> input, ReadOnlySpan<byte> secret, ulong seed)
+ {
+ Debug.Assert(secret.Length >= SecretSizeMin);
+ Debug.Assert(16 < input.Length && input.Length <= 128);
+
+ Hash128 acc = new Hash128
+ {
+ Low = (ulong)input.Length * Prime64_1,
+ High = 0
+ };
+
+ if (input.Length > 32)
+ {
+ if (input.Length > 64)
+ {
+ if (input.Length > 96)
+ {
+ acc = Xxh128Mix32b(acc, input.Slice(48), input.Slice(input.Length - 64), secret.Slice(96), seed);
+ }
+ acc = Xxh128Mix32b(acc, input.Slice(32), input.Slice(input.Length - 48), secret.Slice(64), seed);
+ }
+ acc = Xxh128Mix32b(acc, input.Slice(16), input.Slice(input.Length - 32), secret.Slice(32), seed);
+ }
+ acc = Xxh128Mix32b(acc, input, input.Slice(input.Length - 16), secret, seed);
+
+ Hash128 h128 = new Hash128
+ {
+ Low = acc.Low + acc.High,
+ High = acc.Low * Prime64_1 + acc.High * Prime64_4 + ((ulong)input.Length - seed) * Prime64_2
+ };
+ h128.Low = Xxh3Avalanche(h128.Low);
+ h128.High = 0UL - Xxh3Avalanche(h128.High);
+ return h128;
+ }
+
+ private static Hash128 Xxh3Len129To240128b(ReadOnlySpan<byte> input, ReadOnlySpan<byte> secret, ulong seed)
+ {
+ Debug.Assert(secret.Length >= SecretSizeMin);
+ Debug.Assert(128 < input.Length && input.Length <= 240);
+
+ Hash128 acc = new Hash128();
+
+ int nbRounds = input.Length / 32;
+ acc.Low = (ulong)input.Length * Prime64_1;
+ acc.High = 0;
+
+ for (int i = 0; i < 4; i++)
+ {
+ acc = Xxh128Mix32b(acc, input.Slice(32 * i), input.Slice(32 * i + 16), secret.Slice(32 * i), seed);
+ }
+
+ acc.Low = Xxh3Avalanche(acc.Low);
+ acc.High = Xxh3Avalanche(acc.High);
+ Debug.Assert(nbRounds >= 4);
+
+ for (int i = 4; i < nbRounds; i++)
+ {
+ acc = Xxh128Mix32b(acc, input.Slice(32 * i), input.Slice(32 * i + 16), secret.Slice(MidSizeStartOffset + 32 * (i - 4)), seed);
+ }
+
+ acc = Xxh128Mix32b(acc, input.Slice(input.Length - 16), input.Slice(input.Length - 32), secret.Slice(SecretSizeMin - MidSizeLastOffset - 16), 0UL - seed);
+
+ Hash128 h128 = new Hash128
+ {
+ Low = acc.Low + acc.High,
+ High = acc.Low * Prime64_1 + acc.High * Prime64_4 + ((ulong)input.Length - seed) * Prime64_2
+ };
+ h128.Low = Xxh3Avalanche(h128.Low);
+ h128.High = 0UL - Xxh3Avalanche(h128.High);
+ return h128;
+ }
+
+ private static Hash128 Xxh3128bitsInternal(ReadOnlySpan<byte> input, ReadOnlySpan<byte> secret, ulong seed)
+ {
+ Debug.Assert(secret.Length >= SecretSizeMin);
+
+ if (input.Length <= 16)
+ {
+ return Xxh3Len0To16128b(input, secret, seed);
+ }
+ else if (input.Length <= 128)
+ {
+ return Xxh3Len17To128128b(input, secret, seed);
+ }
+ else if (input.Length <= 240)
+ {
+ return Xxh3Len129To240128b(input, secret, seed);
+ }
+ else
+ {
+ return Xxh3HashLong128bInternal(input, secret);
+ }
+ }
+
+ public static Hash128 ComputeHash(ReadOnlySpan<byte> input)
+ {
+ return Xxh3128bitsInternal(input, Xxh3KSecret, 0UL);
+ }
+ }
+}