From 1ecc8fbc3b395f8238d4e74f06a8c014336d25b7 Mon Sep 17 00:00:00 2001
From: jhorv <38920027+jhorv@users.noreply.github.com>
Date: Sun, 2 Jun 2024 21:24:14 -0400
Subject: New pooled memory types (#6821)

* feat: add new types MemoryOwner and SpanOwner

* use SpanOwner instead of new array allocation

* change for loop condition to `fences.Length` instead of `count` to elide Span boundary checks on `fences`
---
 src/Ryujinx.Common/Memory/MemoryOwner.cs        | 140 ++++++++++++++++++++++++
 src/Ryujinx.Common/Memory/SpanOwner.cs          | 114 +++++++++++++++++++
 src/Ryujinx.Graphics.Vulkan/MultiFenceHolder.cs |   6 +-
 3 files changed, 258 insertions(+), 2 deletions(-)
 create mode 100644 src/Ryujinx.Common/Memory/MemoryOwner.cs
 create mode 100644 src/Ryujinx.Common/Memory/SpanOwner.cs

(limited to 'src')

diff --git a/src/Ryujinx.Common/Memory/MemoryOwner.cs b/src/Ryujinx.Common/Memory/MemoryOwner.cs
new file mode 100644
index 00000000..5e567ab8
--- /dev/null
+++ b/src/Ryujinx.Common/Memory/MemoryOwner.cs
@@ -0,0 +1,140 @@
+#nullable enable
+using System;
+using System.Buffers;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Threading;
+
+namespace Ryujinx.Common.Memory
+{
+    /// <summary>
+    /// An <see cref="IMemoryOwner{T}"/> implementation with an embedded length and fast <see cref="Span{T}"/>
+    /// accessor, with memory allocated from <seealso cref="ArrayPool{T}.Shared"/>.
+    /// </summary>
+    /// <typeparam name="T">The type of item to store.</typeparam>
+    public sealed class MemoryOwner<T> : IMemoryOwner<T>
+    {
+        private readonly int _length;
+        private T[]? _array;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="MemoryOwner{T}"/> class with the specified parameters.
+        /// </summary>
+        /// <param name="length">The length of the new memory buffer to use</param>
+        private MemoryOwner(int length)
+        {
+            _length = length;
+            _array = ArrayPool<T>.Shared.Rent(length);
+        }
+
+        /// <summary>
+        /// Creates a new <see cref="MemoryOwner{T}"/> instance with the specified length.
+        /// </summary>
+        /// <param name="length">The length of the new memory buffer to use</param>
+        /// <returns>A <see cref="MemoryOwner{T}"/> instance of the requested length</returns>
+        /// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="length"/> is not valid</exception>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static MemoryOwner<T> Rent(int length) => new(length);
+
+        /// <summary>
+        /// Creates a new <see cref="MemoryOwner{T}"/> instance with the specified length and the content cleared.
+        /// </summary>
+        /// <param name="length">The length of the new memory buffer to use</param>
+        /// <returns>A <see cref="MemoryOwner{T}"/> instance of the requested length and the content cleared</returns>
+        /// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="length"/> is not valid</exception>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static MemoryOwner<T> RentCleared(int length)
+        {
+            MemoryOwner<T> result = new(length);
+
+            result._array.AsSpan(0, length).Clear();
+
+            return result;
+        }
+
+        /// <summary>
+        /// Creates a new <see cref="MemoryOwner{T}"/> instance with the content copied from the specified buffer.
+        /// </summary>
+        /// <param name="buffer">The buffer to copy</param>
+        /// <returns>A <see cref="MemoryOwner{T}"/> instance with the same length and content as <paramref name="buffer"/></returns>
+        public static MemoryOwner<T> RentCopy(ReadOnlySpan<T> buffer)
+        {
+            MemoryOwner<T> result = new(buffer.Length);
+
+            buffer.CopyTo(result._array);
+
+            return result;
+        }
+
+        /// <summary>
+        /// Gets the number of items in the current instance.
+        /// </summary>
+        public int Length
+        {
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            get => _length;
+        }
+
+        /// <inheritdoc/>
+        public Memory<T> Memory
+        {
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            get
+            {
+                T[]? array = _array;
+
+                if (array is null)
+                {
+                    ThrowObjectDisposedException();
+                }
+
+                return new(array, 0, _length);
+            }
+        }
+
+        /// <summary>
+        /// Gets a <see cref="Span{T}"/> wrapping the memory belonging to the current instance.
+        /// </summary>
+        /// <remarks>
+        /// Uses a trick made possible by the .NET 6+ runtime array layout.
+        /// </remarks>
+        public Span<T> Span
+        {
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            get
+            {
+                T[]? array = _array;
+
+                if (array is null)
+                {
+                    ThrowObjectDisposedException();
+                }
+
+                ref T firstElementRef = ref MemoryMarshal.GetArrayDataReference(array);
+
+                return MemoryMarshal.CreateSpan(ref firstElementRef, _length);
+            }
+        }
+
+        /// <inheritdoc/>
+        public void Dispose()
+        {
+            T[]? array = Interlocked.Exchange(ref _array, null);
+
+            if (array is not null)
+            {
+                ArrayPool<T>.Shared.Return(array);
+            }
+        }
+
+        /// <summary>
+        /// Throws an <see cref="ObjectDisposedException"/> when <see cref="_array"/> is <see langword="null"/>.
+        /// </summary>
+        [DoesNotReturn]
+        private static void ThrowObjectDisposedException()
+        {
+            throw new ObjectDisposedException(nameof(MemoryOwner<T>), "The buffer has already been disposed.");
+        }
+    }
+}
diff --git a/src/Ryujinx.Common/Memory/SpanOwner.cs b/src/Ryujinx.Common/Memory/SpanOwner.cs
new file mode 100644
index 00000000..a4b4adf3
--- /dev/null
+++ b/src/Ryujinx.Common/Memory/SpanOwner.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Buffers;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Common.Memory
+{
+    /// <summary>
+    /// A stack-only type that rents a buffer of a specified length from <seealso cref="ArrayPool{T}.Shared"/>.
+    /// It does not implement <see cref="IDisposable"/> to avoid being boxed, but should still be disposed. This
+    /// is easy since C# 8, which allows use of C# `using` constructs on any type that has a public Dispose() method.
+    /// To keep this type simple, fast, and read-only, it does not check or guard against multiple disposals.
+    /// For all these reasons, all usage should be with a `using` block or statement.
+    /// </summary>
+    /// <typeparam name="T">The type of item to store.</typeparam>
+    public readonly ref struct SpanOwner<T>
+    {
+        private readonly int _length;
+        private readonly T[] _array;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SpanOwner{T}"/> struct with the specified parameters.
+        /// </summary>
+        /// <param name="length">The length of the new memory buffer to use</param>
+        private SpanOwner(int length)
+        {
+            _length = length;
+            _array = ArrayPool<T>.Shared.Rent(length);
+        }
+
+        /// <summary>
+        /// Gets an empty <see cref="SpanOwner{T}"/> instance.
+        /// </summary>
+        public static SpanOwner<T> Empty
+        {
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            get => new(0);
+        }
+
+        /// <summary>
+        /// Creates a new <see cref="SpanOwner{T}"/> instance with the specified length.
+        /// </summary>
+        /// <param name="length">The length of the new memory buffer to use</param>
+        /// <returns>A <see cref="SpanOwner{T}"/> instance of the requested length</returns>
+        /// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="length"/> is not valid</exception>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static SpanOwner<T> Rent(int length) => new(length);
+
+        /// <summary>
+        /// Creates a new <see cref="SpanOwner{T}"/> instance with the length and the content cleared.
+        /// </summary>
+        /// <param name="length">The length of the new memory buffer to use</param>
+        /// <returns>A <see cref="SpanOwner{T}"/> instance of the requested length and the content cleared</returns>
+        /// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="length"/> is not valid</exception>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static SpanOwner<T> RentCleared(int length)
+        {
+            SpanOwner<T> result = new(length);
+
+            result._array.AsSpan(0, length).Clear();
+
+            return result;
+        }
+
+        /// <summary>
+        /// Creates a new <see cref="SpanOwner{T}"/> instance with the content copied from the specified buffer.
+        /// </summary>
+        /// <param name="buffer">The buffer to copy</param>
+        /// <returns>A <see cref="SpanOwner{T}"/> instance with the same length and content as <paramref name="buffer"/></returns>
+        public static SpanOwner<T> RentCopy(ReadOnlySpan<T> buffer)
+        {
+            SpanOwner<T> result = new(buffer.Length);
+
+            buffer.CopyTo(result._array);
+
+            return result;
+        }
+
+        /// <summary>
+        /// Gets the number of items in the current instance
+        /// </summary>
+        public int Length
+        {
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            get => _length;
+        }
+
+        /// <summary>
+        /// Gets a <see cref="Span{T}"/> wrapping the memory belonging to the current instance.
+        /// </summary>
+        /// <remarks>
+        /// Uses a trick made possible by the .NET 6+ runtime array layout.
+        /// </remarks>
+        public Span<T> Span
+        {
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            get
+            {
+                ref T firstElementRef = ref MemoryMarshal.GetArrayDataReference(_array);
+
+                return MemoryMarshal.CreateSpan(ref firstElementRef, _length);
+            }
+        }
+
+        /// <summary>
+        /// Implements the duck-typed <see cref="IDisposable.Dispose"/> method.
+        /// </summary>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public void Dispose()
+        {
+            ArrayPool<T>.Shared.Return(_array);
+        }
+    }
+}
diff --git a/src/Ryujinx.Graphics.Vulkan/MultiFenceHolder.cs b/src/Ryujinx.Graphics.Vulkan/MultiFenceHolder.cs
index 0bce3b72..806b872b 100644
--- a/src/Ryujinx.Graphics.Vulkan/MultiFenceHolder.cs
+++ b/src/Ryujinx.Graphics.Vulkan/MultiFenceHolder.cs
@@ -1,3 +1,4 @@
+using Ryujinx.Common.Memory;
 using Silk.NET.Vulkan;
 using System;
 
@@ -165,14 +166,15 @@ namespace Ryujinx.Graphics.Vulkan
         /// <returns>True if all fences were signaled before the timeout expired, false otherwise</returns>
         private bool WaitForFencesImpl(Vk api, Device device, int offset, int size, bool hasTimeout, ulong timeout)
         {
-            Span<FenceHolder> fenceHolders = new FenceHolder[CommandBufferPool.MaxCommandBuffers];
+            using SpanOwner<FenceHolder> fenceHoldersOwner = SpanOwner<FenceHolder>.Rent(CommandBufferPool.MaxCommandBuffers);
+            Span<FenceHolder> fenceHolders = fenceHoldersOwner.Span;
 
             int count = size != 0 ? GetOverlappingFences(fenceHolders, offset, size) : GetFences(fenceHolders);
             Span<Fence> fences = stackalloc Fence[count];
 
             int fenceCount = 0;
 
-            for (int i = 0; i < count; i++)
+            for (int i = 0; i < fences.Length; i++)
             {
                 if (fenceHolders[i].TryGet(out Fence fence))
                 {
-- 
cgit v1.2.3-70-g09d2