Ryujinx/src/Ryujinx.Tests/Common/Extensions/SequenceReaderExtensionsTests.cs
jhorv ead9a25141
Audio rendering: reduce memory allocations (#6604)
* - WritableRegion: enable wrapping IMemoryOwner<byte>
- IVirtualMemoryManager impls of GetWritableRegion() use pooled memory when region is non-contiguous.
- IVirtualMemoryManager: add GetReadOnlySequence() and impls
- ByteMemoryPool: add new method RentCopy()
- ByteMemoryPool: make class static, remove ctor and singleton field from earlier impl

* - BytesReadOnlySequenceSegment: move from Ryujinx.Common.Memory to Ryujinx.Memory
- BytesReadOnlySequenceSegment: add IsContiguousWith() and Replace() methods
- VirtualMemoryManagerBase:
  - remove generic type parameters, instead use ulong for virtual addresses and nuint for host/physical addresses
  - implement IWritableBlock
  - add virtual GetReadOnlySequence() with coalescing of contiguous segments
  - add virtual GetSpan()
  - add virtual GetWritableRegion()
  - add abstract IsMapped()
  - add virtual MapForeign(ulong, nuint, ulong)
  - add virtual Read<T>()
  - add virtual Read(ulong, Span<byte>)
  - add virtual ReadTracked<T>()
  - add virtual SignalMemoryTracking()
  - add virtual Write()
  - add virtual Write<T>()
  - add virtual WriteUntracked()
  - add virtual WriteWithRedundancyCheck()
- VirtualMemoryManagerRefCountedBase: remove generic type parameters
- AddressSpaceManager: remove redundant methods, add required overrides
- HvMemoryManager: remove redundant methods, add required overrides, add overrides for _invalidAccessHandler handling
- MemoryManager: remove redundant methods, add required overrides, add overrides for _invalidAccessHandler handling
- MemoryManagerHostMapped: remove redundant methods, add required overrides, add overrides for _invalidAccessHandler handling
- NativeMemoryManager: add get properties for Pointer and Length
- throughout: removed invalid <inheritdoc/> comments

* - WritableRegion: enable wrapping IMemoryOwner<byte>
- IVirtualMemoryManager impls of GetWritableRegion() use pooled memory when region is non-contiguous.
- IVirtualMemoryManager: add GetReadOnlySequence() and impls
- ByteMemoryPool: add new method RentCopy()
- ByteMemoryPool: make class static, remove ctor and singleton field from earlier impl

* add PagedMemoryRange enumerator types, use them in IVirtualMemoryManager implementations to consolidate page-handling logic and add a new capability - the coalescing of pages for consolidating memory copies and segmentation.

* new: more tests for PagedMemoryRangeCoalescingEnumerator showing coalescing of contiguous segments

* make some struct properties readonly

* put braces around `foreach` bodies

* encourage inlining of some PagedMemoryRange*Enumerator members

* DynamicRingBuffer:
 - use ByteMemoryPool
 - make some methods return without locking when size/count argument = 0
 - make generic Read<T>()/Write<T>() non-generic because its only usage is as T = byte
 - change Read(byte[]...) to Read(Span<byte>...)
 - change Write(byte[]...) to Write(Span<byte>...)

* change IAudioRenderer.RequestUpdate() to take a ReadOnlySequence<byte>, enabling zero-copy audio rendering

* HipcGenerator: support ReadOnlySequence<byte> as IPC method parameter

* change IAudioRenderer/AudioRenderer RequestUpdate* methods to take input as ReadOnlySequence<byte>

* MemoryManagerHostTracked: use rented memory when contiguous in `GetWritableRegion()`

* rebase cleanup

* dotnet format fixes

* format and comment fixes

* format long parameter list - take 2

* - add support to HipcGenerator for buffers of type `Memory<byte>`
- change `AudioRenderer` `RequestUpdate()` and `RequestUpdateAuto()` to use Memory<byte> for output buffers, removing another memory block allocation/copy

* SplitterContext `UpdateState()` and `UpdateData()` smooth out advance/rewind logic, only rewind if magic is invalid

* DynamicRingBuffer.Write(): change Span<byte> to ReadOnlySpan<byte>
2024-04-07 18:07:32 -03:00

360 lines
13 KiB
C#

using NUnit.Framework;
using Ryujinx.Common.Extensions;
using Ryujinx.Memory;
using System;
using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ryujinx.Tests.Common.Extensions
{
public class SequenceReaderExtensionsTests
{
[TestCase(null)]
[TestCase(sizeof(int) + 1)]
public void GetRefOrRefToCopy_ReadsMultiSegmentedSequenceSuccessfully(int? maxSegmentSize)
{
// Arrange
MyUnmanagedStruct[] originalStructs = EnumerateNewUnmanagedStructs().Take(3).ToArray();
ReadOnlySequence<byte> sequence =
CreateSegmentedByteSequence(originalStructs, maxSegmentSize ?? Unsafe.SizeOf<MyUnmanagedStruct>());
var sequenceReader = new SequenceReader<byte>(sequence);
foreach (var original in originalStructs)
{
// Act
ref readonly MyUnmanagedStruct read = ref sequenceReader.GetRefOrRefToCopy<MyUnmanagedStruct>(out _);
// Assert
MyUnmanagedStruct.Assert(Assert.AreEqual, original, read);
}
}
[Test]
public void GetRefOrRefToCopy_FragmentedSequenceReturnsRefToCopy()
{
// Arrange
MyUnmanagedStruct[] originalStructs = EnumerateNewUnmanagedStructs().Take(1).ToArray();
ReadOnlySequence<byte> sequence = CreateSegmentedByteSequence(originalStructs, 3);
var sequenceReader = new SequenceReader<byte>(sequence);
foreach (var original in originalStructs)
{
// Act
ref readonly MyUnmanagedStruct read = ref sequenceReader.GetRefOrRefToCopy<MyUnmanagedStruct>(out var copy);
// Assert
MyUnmanagedStruct.Assert(Assert.AreEqual, original, read);
MyUnmanagedStruct.Assert(Assert.AreEqual, read, copy);
}
}
[Test]
public void GetRefOrRefToCopy_ContiguousSequenceReturnsRefToBuffer()
{
// Arrange
MyUnmanagedStruct[] originalStructs = EnumerateNewUnmanagedStructs().Take(1).ToArray();
ReadOnlySequence<byte> sequence = CreateSegmentedByteSequence(originalStructs, int.MaxValue);
var sequenceReader = new SequenceReader<byte>(sequence);
foreach (var original in originalStructs)
{
// Act
ref readonly MyUnmanagedStruct read = ref sequenceReader.GetRefOrRefToCopy<MyUnmanagedStruct>(out var copy);
// Assert
MyUnmanagedStruct.Assert(Assert.AreEqual, original, read);
MyUnmanagedStruct.Assert(Assert.AreNotEqual, read, copy);
}
}
[Test]
public void GetRefOrRefToCopy_ThrowsWhenNotEnoughData()
{
// Arrange
MyUnmanagedStruct[] originalStructs = EnumerateNewUnmanagedStructs().Take(1).ToArray();
ReadOnlySequence<byte> sequence = CreateSegmentedByteSequence(originalStructs, int.MaxValue);
// Act/Assert
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
var sequenceReader = new SequenceReader<byte>(sequence);
sequenceReader.Advance(1);
ref readonly MyUnmanagedStruct result = ref sequenceReader.GetRefOrRefToCopy<MyUnmanagedStruct>(out _);
});
}
[Test]
public void ReadLittleEndian_Int32_RoundTripsSuccessfully()
{
// Arrange
const int TestValue = 0x1234abcd;
byte[] buffer = new byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(), TestValue);
var sequenceReader = new SequenceReader<byte>(new ReadOnlySequence<byte>(buffer));
// Act
sequenceReader.ReadLittleEndian(out int roundTrippedValue);
// Assert
Assert.AreEqual(TestValue, roundTrippedValue);
}
[Test]
public void ReadLittleEndian_Int32_ResultIsNotBigEndian()
{
// Arrange
const int TestValue = 0x1234abcd;
byte[] buffer = new byte[sizeof(int)];
BinaryPrimitives.WriteInt32BigEndian(buffer.AsSpan(), TestValue);
var sequenceReader = new SequenceReader<byte>(new ReadOnlySequence<byte>(buffer));
// Act
sequenceReader.ReadLittleEndian(out int roundTrippedValue);
// Assert
Assert.AreNotEqual(TestValue, roundTrippedValue);
}
[Test]
public void ReadLittleEndian_Int32_ThrowsWhenNotEnoughData()
{
// Arrange
const int TestValue = 0x1234abcd;
byte[] buffer = new byte[sizeof(int)];
BinaryPrimitives.WriteInt32BigEndian(buffer.AsSpan(), TestValue);
// Act/Assert
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
var sequenceReader = new SequenceReader<byte>(new ReadOnlySequence<byte>(buffer));
sequenceReader.Advance(1);
sequenceReader.ReadLittleEndian(out int roundTrippedValue);
});
}
[Test]
public void ReadUnmanaged_ContiguousSequence_Succeeds()
=> ReadUnmanaged_Succeeds(int.MaxValue);
[Test]
public void ReadUnmanaged_FragmentedSequence_Succeeds()
=> ReadUnmanaged_Succeeds(sizeof(int) + 1);
[Test]
public void ReadUnmanaged_ThrowsWhenNotEnoughData()
{
// Arrange
MyUnmanagedStruct[] originalStructs = EnumerateNewUnmanagedStructs().Take(1).ToArray();
ReadOnlySequence<byte> sequence = CreateSegmentedByteSequence(originalStructs, int.MaxValue);
// Act/Assert
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
var sequenceReader = new SequenceReader<byte>(sequence);
sequenceReader.Advance(1);
sequenceReader.ReadUnmanaged(out MyUnmanagedStruct read);
});
}
[Test]
public void SetConsumed_ContiguousSequence_SucceedsWhenValid()
=> SetConsumed_SucceedsWhenValid(int.MaxValue);
[Test]
public void SetConsumed_FragmentedSequence_SucceedsWhenValid()
=> SetConsumed_SucceedsWhenValid(sizeof(int) + 1);
[Test]
public void SetConsumed_ThrowsWhenBeyondActualLength()
{
const int StructCount = 2;
// Arrange
MyUnmanagedStruct[] originalStructs = EnumerateNewUnmanagedStructs().Take(StructCount).ToArray();
ReadOnlySequence<byte> sequence = CreateSegmentedByteSequence(originalStructs, MyUnmanagedStruct.SizeOf);
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
var sequenceReader = new SequenceReader<byte>(sequence);
sequenceReader.SetConsumed(MyUnmanagedStruct.SizeOf * StructCount + 1);
});
}
private static void ReadUnmanaged_Succeeds(int maxSegmentLength)
{
// Arrange
MyUnmanagedStruct[] originalStructs = EnumerateNewUnmanagedStructs().Take(3).ToArray();
ReadOnlySequence<byte> sequence = CreateSegmentedByteSequence(originalStructs, maxSegmentLength);
var sequenceReader = new SequenceReader<byte>(sequence);
foreach (var original in originalStructs)
{
// Act
sequenceReader.ReadUnmanaged(out MyUnmanagedStruct read);
// Assert
MyUnmanagedStruct.Assert(Assert.AreEqual, original, read);
}
}
private static void SetConsumed_SucceedsWhenValid(int maxSegmentLength)
{
// Arrange
MyUnmanagedStruct[] originalStructs = EnumerateNewUnmanagedStructs().Take(2).ToArray();
ReadOnlySequence<byte> sequence = CreateSegmentedByteSequence(originalStructs, maxSegmentLength);
var sequenceReader = new SequenceReader<byte>(sequence);
static void SetConsumedAndAssert(scoped ref SequenceReader<byte> sequenceReader, long consumed)
{
sequenceReader.SetConsumed(consumed);
Assert.AreEqual(consumed, sequenceReader.Consumed);
}
// Act/Assert
ref readonly MyUnmanagedStruct struct0A = ref sequenceReader.GetRefOrRefToCopy<MyUnmanagedStruct>(out _);
Assert.AreEqual(sequenceReader.Consumed, MyUnmanagedStruct.SizeOf);
SetConsumedAndAssert(ref sequenceReader, 0);
ref readonly MyUnmanagedStruct struct0B = ref sequenceReader.GetRefOrRefToCopy<MyUnmanagedStruct>(out _);
MyUnmanagedStruct.Assert(Assert.AreEqual, struct0A, struct0B);
SetConsumedAndAssert(ref sequenceReader, 1);
SetConsumedAndAssert(ref sequenceReader, MyUnmanagedStruct.SizeOf);
ref readonly MyUnmanagedStruct struct1A = ref sequenceReader.GetRefOrRefToCopy<MyUnmanagedStruct>(out _);
SetConsumedAndAssert(ref sequenceReader, MyUnmanagedStruct.SizeOf);
ref readonly MyUnmanagedStruct struct1B = ref sequenceReader.GetRefOrRefToCopy<MyUnmanagedStruct>(out _);
MyUnmanagedStruct.Assert(Assert.AreEqual, struct1A, struct1B);
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private struct MyUnmanagedStruct
{
public int BehaviourSize;
public int MemoryPoolsSize;
public short VoicesSize;
public int VoiceResourcesSize;
public short EffectsSize;
public int RenderInfoSize;
public unsafe fixed byte Reserved[16];
public static readonly int SizeOf = Unsafe.SizeOf<MyUnmanagedStruct>();
public static unsafe MyUnmanagedStruct Generate(Random rng)
{
const int BaseInt32Value = 0x1234abcd;
const short BaseInt16Value = 0x5678;
var result = new MyUnmanagedStruct
{
BehaviourSize = BaseInt32Value ^ rng.Next(),
MemoryPoolsSize = BaseInt32Value ^ rng.Next(),
VoicesSize = (short)(BaseInt16Value ^ rng.Next()),
VoiceResourcesSize = BaseInt32Value ^ rng.Next(),
EffectsSize = (short)(BaseInt16Value ^ rng.Next()),
RenderInfoSize = BaseInt32Value ^ rng.Next(),
};
Unsafe.Write(result.Reserved, rng.NextInt64());
return result;
}
public static unsafe void Assert(Action<object, object> assert, in MyUnmanagedStruct expected, in MyUnmanagedStruct actual)
{
assert(expected.BehaviourSize, actual.BehaviourSize);
assert(expected.MemoryPoolsSize, actual.MemoryPoolsSize);
assert(expected.VoicesSize, actual.VoicesSize);
assert(expected.VoiceResourcesSize, actual.VoiceResourcesSize);
assert(expected.EffectsSize, actual.EffectsSize);
assert(expected.RenderInfoSize, actual.RenderInfoSize);
fixed (void* expectedReservedPtr = expected.Reserved)
fixed (void* actualReservedPtr = actual.Reserved)
{
long expectedReservedLong = Unsafe.Read<long>(expectedReservedPtr);
long actualReservedLong = Unsafe.Read<long>(actualReservedPtr);
assert(expectedReservedLong, actualReservedLong);
}
}
}
private static IEnumerable<MyUnmanagedStruct> EnumerateNewUnmanagedStructs()
{
var rng = new Random(0);
while (true)
{
yield return MyUnmanagedStruct.Generate(rng);
}
}
private static ReadOnlySequence<byte> CreateSegmentedByteSequence<T>(T[] array, int maxSegmentLength) where T : unmanaged
{
byte[] arrayBytes = MemoryMarshal.AsBytes(array.AsSpan()).ToArray();
var memory = new Memory<byte>(arrayBytes);
int index = 0;
BytesReadOnlySequenceSegment first = null, last = null;
while (index < memory.Length)
{
int nextSegmentLength = Math.Min(maxSegmentLength, memory.Length - index);
var nextSegment = memory.Slice(index, nextSegmentLength);
if (first == null)
{
first = last = new BytesReadOnlySequenceSegment(nextSegment);
}
else
{
last = last.Append(nextSegment);
}
index += nextSegmentLength;
}
return new ReadOnlySequence<byte>(first, 0, last, (int)(memory.Length - last.RunningIndex));
}
}
}