diff options
| author | TSR Berry <20988865+TSRBerry@users.noreply.github.com> | 2023-04-08 01:22:00 +0200 |
|---|---|---|
| committer | Mary <thog@protonmail.com> | 2023-04-27 23:51:14 +0200 |
| commit | cee712105850ac3385cd0091a923438167433f9f (patch) | |
| tree | 4a5274b21d8b7f938c0d0ce18736d3f2993b11b1 /src/Ryujinx.Common | |
| parent | cd124bda587ef09668a971fa1cac1c3f0cfc9f21 (diff) | |
Move solution and projects to src
Diffstat (limited to 'src/Ryujinx.Common')
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); + } + } +} |
