Просмотр исходного кода

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>
jhorv 2 лет назад
Родитель
Сommit
ead9a25141
32 измененных файлов с 847 добавлено и 262 удалено
  1. 35 28
      src/Ryujinx.Audio/Backends/Common/DynamicRingBuffer.cs
  2. 1 1
      src/Ryujinx.Audio/Renderer/Common/BehaviourParameter.cs
  3. 1 1
      src/Ryujinx.Audio/Renderer/Common/UpdateDataHeader.cs
  4. 1 1
      src/Ryujinx.Audio/Renderer/Parameter/BehaviourErrorInfoOutStatus.cs
  5. 6 4
      src/Ryujinx.Audio/Renderer/Server/AudioRenderSystem.cs
  6. 2 1
      src/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs
  7. 7 7
      src/Ryujinx.Audio/Renderer/Server/Effect/AuxiliaryBufferEffect.cs
  8. 7 7
      src/Ryujinx.Audio/Renderer/Server/Effect/BaseEffect.cs
  9. 7 7
      src/Ryujinx.Audio/Renderer/Server/Effect/BiquadFilterEffect.cs
  10. 7 7
      src/Ryujinx.Audio/Renderer/Server/Effect/BufferMixEffect.cs
  11. 7 7
      src/Ryujinx.Audio/Renderer/Server/Effect/CaptureBufferEffect.cs
  12. 4 4
      src/Ryujinx.Audio/Renderer/Server/Effect/CompressorEffect.cs
  13. 7 7
      src/Ryujinx.Audio/Renderer/Server/Effect/DelayEffect.cs
  14. 7 7
      src/Ryujinx.Audio/Renderer/Server/Effect/LimiterEffect.cs
  15. 7 7
      src/Ryujinx.Audio/Renderer/Server/Effect/Reverb3dEffect.cs
  16. 7 7
      src/Ryujinx.Audio/Renderer/Server/Effect/ReverbEffect.cs
  17. 1 1
      src/Ryujinx.Audio/Renderer/Server/MemoryPool/PoolMapper.cs
  18. 3 3
      src/Ryujinx.Audio/Renderer/Server/Mix/MixState.cs
  19. 4 4
      src/Ryujinx.Audio/Renderer/Server/Sink/BaseSink.cs
  20. 3 3
      src/Ryujinx.Audio/Renderer/Server/Sink/CircularBufferSink.cs
  21. 3 3
      src/Ryujinx.Audio/Renderer/Server/Sink/DeviceSink.cs
  22. 31 24
      src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterContext.cs
  23. 7 4
      src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterState.cs
  24. 71 83
      src/Ryujinx.Audio/Renderer/Server/StateUpdater.cs
  25. 19 7
      src/Ryujinx.Audio/Renderer/Server/Voice/VoiceState.cs
  26. 181 0
      src/Ryujinx.Common/Extensions/SequenceReaderExtensions.cs
  27. 5 3
      src/Ryujinx.Cpu/Jit/MemoryManagerHostTracked.cs
  28. 31 3
      src/Ryujinx.Horizon.Generators/Hipc/HipcGenerator.cs
  29. 7 19
      src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioRenderer.cs
  30. 3 2
      src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioRenderer.cs
  31. 6 0
      src/Ryujinx.Horizon/Sdk/Sf/CommandSerialization.cs
  32. 359 0
      src/Ryujinx.Tests/Common/Extensions/SequenceReaderExtensionsTests.cs

+ 35 - 28
src/Ryujinx.Audio/Backends/Common/DynamicRingBuffer.cs

@@ -1,5 +1,7 @@
 using Ryujinx.Common;
+using Ryujinx.Common.Memory;
 using System;
+using System.Buffers;
 
 namespace Ryujinx.Audio.Backends.Common
 {
@@ -12,7 +14,8 @@ namespace Ryujinx.Audio.Backends.Common
 
         private readonly object _lock = new();
 
-        private byte[] _buffer;
+        private IMemoryOwner<byte> _bufferOwner;
+        private Memory<byte> _buffer;
         private int _size;
         private int _headOffset;
         private int _tailOffset;
@@ -21,7 +24,8 @@ namespace Ryujinx.Audio.Backends.Common
 
         public DynamicRingBuffer(int initialCapacity = RingBufferAlignment)
         {
-            _buffer = new byte[initialCapacity];
+            _bufferOwner = ByteMemoryPool.RentCleared(initialCapacity);
+            _buffer = _bufferOwner.Memory;
         }
 
         public void Clear()
@@ -33,6 +37,11 @@ namespace Ryujinx.Audio.Backends.Common
 
         public void Clear(int size)
         {
+            if (size == 0)
+            {
+                return;
+            }
+
             lock (_lock)
             {
                 if (size > _size)
@@ -40,11 +49,6 @@ namespace Ryujinx.Audio.Backends.Common
                     size = _size;
                 }
 
-                if (size == 0)
-                {
-                    return;
-                }
-
                 _headOffset = (_headOffset + size) % _buffer.Length;
                 _size -= size;
 
@@ -58,28 +62,31 @@ namespace Ryujinx.Audio.Backends.Common
 
         private void SetCapacityLocked(int capacity)
         {
-            byte[] buffer = new byte[capacity];
+            IMemoryOwner<byte> newBufferOwner = ByteMemoryPool.RentCleared(capacity);
+            Memory<byte> newBuffer = newBufferOwner.Memory;
 
             if (_size > 0)
             {
                 if (_headOffset < _tailOffset)
                 {
-                    Buffer.BlockCopy(_buffer, _headOffset, buffer, 0, _size);
+                    _buffer.Slice(_headOffset, _size).CopyTo(newBuffer);
                 }
                 else
                 {
-                    Buffer.BlockCopy(_buffer, _headOffset, buffer, 0, _buffer.Length - _headOffset);
-                    Buffer.BlockCopy(_buffer, 0, buffer, _buffer.Length - _headOffset, _tailOffset);
+                    _buffer[_headOffset..].CopyTo(newBuffer);
+                    _buffer[.._tailOffset].CopyTo(newBuffer[(_buffer.Length - _headOffset)..]);
                 }
             }
 
-            _buffer = buffer;
+            _bufferOwner.Dispose();
+
+            _bufferOwner = newBufferOwner;
+            _buffer = newBuffer;
             _headOffset = 0;
             _tailOffset = _size;
         }
 
-
-        public void Write<T>(T[] buffer, int index, int count)
+        public void Write(ReadOnlySpan<byte> buffer, int index, int count)
         {
             if (count == 0)
             {
@@ -99,17 +106,17 @@ namespace Ryujinx.Audio.Backends.Common
 
                     if (tailLength >= count)
                     {
-                        Buffer.BlockCopy(buffer, index, _buffer, _tailOffset, count);
+                        buffer.Slice(index, count).CopyTo(_buffer.Span[_tailOffset..]);
                     }
                     else
                     {
-                        Buffer.BlockCopy(buffer, index, _buffer, _tailOffset, tailLength);
-                        Buffer.BlockCopy(buffer, index + tailLength, _buffer, 0, count - tailLength);
+                        buffer.Slice(index, tailLength).CopyTo(_buffer.Span[_tailOffset..]);
+                        buffer.Slice(index + tailLength, count - tailLength).CopyTo(_buffer.Span);
                     }
                 }
                 else
                 {
-                    Buffer.BlockCopy(buffer, index, _buffer, _tailOffset, count);
+                    buffer.Slice(index, count).CopyTo(_buffer.Span[_tailOffset..]);
                 }
 
                 _size += count;
@@ -117,8 +124,13 @@ namespace Ryujinx.Audio.Backends.Common
             }
         }
 
-        public int Read<T>(T[] buffer, int index, int count)
+        public int Read(Span<byte> buffer, int index, int count)
         {
+            if (count == 0)
+            {
+                return 0;
+            }
+
             lock (_lock)
             {
                 if (count > _size)
@@ -126,14 +138,9 @@ namespace Ryujinx.Audio.Backends.Common
                     count = _size;
                 }
 
-                if (count == 0)
-                {
-                    return 0;
-                }
-
                 if (_headOffset < _tailOffset)
                 {
-                    Buffer.BlockCopy(_buffer, _headOffset, buffer, index, count);
+                    _buffer.Span.Slice(_headOffset, count).CopyTo(buffer[index..]);
                 }
                 else
                 {
@@ -141,12 +148,12 @@ namespace Ryujinx.Audio.Backends.Common
 
                     if (tailLength >= count)
                     {
-                        Buffer.BlockCopy(_buffer, _headOffset, buffer, index, count);
+                        _buffer.Span.Slice(_headOffset, count).CopyTo(buffer[index..]);
                     }
                     else
                     {
-                        Buffer.BlockCopy(_buffer, _headOffset, buffer, index, tailLength);
-                        Buffer.BlockCopy(_buffer, 0, buffer, index + tailLength, count - tailLength);
+                        _buffer.Span.Slice(_headOffset, tailLength).CopyTo(buffer[index..]);
+                        _buffer.Span[..(count - tailLength)].CopyTo(buffer[(index + tailLength)..]);
                     }
                 }
 

+ 1 - 1
src/Ryujinx.Audio/Renderer/Common/BehaviourParameter.cs

@@ -25,7 +25,7 @@ namespace Ryujinx.Audio.Renderer.Common
         public ulong Flags;
 
         /// <summary>
-        /// Represents an error during <see cref="Server.AudioRenderSystem.Update(System.Memory{byte}, System.Memory{byte}, System.ReadOnlyMemory{byte})"/>.
+        /// Represents an error during <see cref="Server.AudioRenderSystem.Update(System.Memory{byte}, System.Memory{byte}, System.Buffers.ReadOnlySequence{byte})"/>.
         /// </summary>
         [StructLayout(LayoutKind.Sequential, Pack = 1)]
         public struct ErrorInfo

+ 1 - 1
src/Ryujinx.Audio/Renderer/Common/UpdateDataHeader.cs

@@ -4,7 +4,7 @@ using System.Runtime.CompilerServices;
 namespace Ryujinx.Audio.Renderer.Common
 {
     /// <summary>
-    /// Update data header used for input and output of <see cref="Server.AudioRenderSystem.Update(System.Memory{byte}, System.Memory{byte}, System.ReadOnlyMemory{byte})"/>.
+    /// Update data header used for input and output of <see cref="Server.AudioRenderSystem.Update(System.Memory{byte}, System.Memory{byte}, System.Buffers.ReadOnlySequence{byte})"/>.
     /// </summary>
     public struct UpdateDataHeader
     {

+ 1 - 1
src/Ryujinx.Audio/Renderer/Parameter/BehaviourErrorInfoOutStatus.cs

@@ -8,7 +8,7 @@ namespace Ryujinx.Audio.Renderer.Parameter
     /// <summary>
     /// Output information for behaviour.
     /// </summary>
-    /// <remarks>This is used to report errors to the user during <see cref="Server.AudioRenderSystem.Update(Memory{byte}, Memory{byte}, ReadOnlyMemory{byte})"/> processing.</remarks>
+    /// <remarks>This is used to report errors to the user during <see cref="Server.AudioRenderSystem.Update(Memory{byte}, Memory{byte}, System.Buffers.ReadOnlySequence{byte})"/> processing.</remarks>
     [StructLayout(LayoutKind.Sequential, Pack = 1)]
     public struct BehaviourErrorInfoOutStatus
     {

+ 6 - 4
src/Ryujinx.Audio/Renderer/Server/AudioRenderSystem.cs

@@ -386,7 +386,7 @@ namespace Ryujinx.Audio.Renderer.Server
             }
         }
 
-        public ResultCode Update(Memory<byte> output, Memory<byte> performanceOutput, ReadOnlyMemory<byte> input)
+        public ResultCode Update(Memory<byte> output, Memory<byte> performanceOutput, ReadOnlySequence<byte> input)
         {
             lock (_lock)
             {
@@ -419,14 +419,16 @@ namespace Ryujinx.Audio.Renderer.Server
                     return result;
                 }
 
-                result = stateUpdater.UpdateVoices(_voiceContext, _memoryPools);
+                PoolMapper poolMapper = new PoolMapper(_processHandle, _memoryPools, _behaviourContext.IsMemoryPoolForceMappingEnabled());
+
+                result = stateUpdater.UpdateVoices(_voiceContext, poolMapper);
 
                 if (result != ResultCode.Success)
                 {
                     return result;
                 }
 
-                result = stateUpdater.UpdateEffects(_effectContext, _isActive, _memoryPools);
+                result = stateUpdater.UpdateEffects(_effectContext, _isActive, poolMapper);
 
                 if (result != ResultCode.Success)
                 {
@@ -450,7 +452,7 @@ namespace Ryujinx.Audio.Renderer.Server
                     return result;
                 }
 
-                result = stateUpdater.UpdateSinks(_sinkContext, _memoryPools);
+                result = stateUpdater.UpdateSinks(_sinkContext, poolMapper);
 
                 if (result != ResultCode.Success)
                 {

+ 2 - 1
src/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Buffers;
 using System.Diagnostics;
 using static Ryujinx.Audio.Renderer.Common.BehaviourParameter;
 
@@ -273,7 +274,7 @@ namespace Ryujinx.Audio.Renderer.Server
         }
 
         /// <summary>
-        /// Check if the audio renderer should trust the user destination count in <see cref="Splitter.SplitterState.Update(Splitter.SplitterContext, ref Parameter.SplitterInParameter, ReadOnlySpan{byte})"/>.
+        /// Check if the audio renderer should trust the user destination count in <see cref="Renderer.Server.Splitter.SplitterState.Update(Renderer.Server.Splitter.SplitterContext, Renderer.Parameter.SplitterInParameter, SequenceReader{byte})"/>.
         /// </summary>
         /// <returns>True if the audio renderer should trust the user destination count.</returns>
         public bool IsSplitterBugFixed()

+ 7 - 7
src/Ryujinx.Audio/Renderer/Server/Effect/AuxiliaryBufferEffect.cs

@@ -33,21 +33,21 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
             return WorkBuffers[index].GetReference(true);
         }
 
-        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
+        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
         {
-            Update(out updateErrorInfo, ref parameter, mapper);
+            Update(out updateErrorInfo, in parameter, mapper);
         }
 
-        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
+        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
         {
-            Update(out updateErrorInfo, ref parameter, mapper);
+            Update(out updateErrorInfo, in parameter, mapper);
         }
 
-        public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
+        public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
         {
-            Debug.Assert(IsTypeValid(ref parameter));
+            Debug.Assert(IsTypeValid(in parameter));
 
-            UpdateParameterBase(ref parameter);
+            UpdateParameterBase(in parameter);
 
             Parameter = MemoryMarshal.Cast<byte, AuxiliaryBufferParameter>(parameter.SpecificData)[0];
             IsEnabled = parameter.IsEnabled;

+ 7 - 7
src/Ryujinx.Audio/Renderer/Server/Effect/BaseEffect.cs

@@ -81,7 +81,7 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
         /// </summary>
         /// <param name="parameter">The user parameter.</param>
         /// <returns>Returns true if the <see cref="EffectType"/> sent by the user matches the internal <see cref="EffectType"/>.</returns>
-        public bool IsTypeValid<T>(ref T parameter) where T : unmanaged, IEffectInParameter
+        public bool IsTypeValid<T>(in T parameter) where T : unmanaged, IEffectInParameter
         {
             return parameter.Type == TargetEffectType;
         }
@@ -98,7 +98,7 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
         /// Update the internal common parameters from a user parameter.
         /// </summary>
         /// <param name="parameter">The user parameter.</param>
-        protected void UpdateParameterBase<T>(ref T parameter) where T : unmanaged, IEffectInParameter
+        protected void UpdateParameterBase<T>(in T parameter) where T : unmanaged, IEffectInParameter
         {
             MixId = parameter.MixId;
             ProcessingOrder = parameter.ProcessingOrder;
@@ -139,7 +139,7 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
         /// <summary>
         /// Initialize the given <paramref name="state"/> result state.
         /// </summary>
-        /// <param name="state">The state to initalize</param>
+        /// <param name="state">The state to initialize</param>
         public virtual void InitializeResultState(ref EffectResultState state) { }
 
         /// <summary>
@@ -155,9 +155,9 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
         /// <param name="updateErrorInfo">The possible <see cref="ErrorInfo"/> that was generated.</param>
         /// <param name="parameter">The user parameter.</param>
         /// <param name="mapper">The mapper to use.</param>
-        public virtual void Update(out ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
+        public virtual void Update(out ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
         {
-            Debug.Assert(IsTypeValid(ref parameter));
+            Debug.Assert(IsTypeValid(in parameter));
 
             updateErrorInfo = new ErrorInfo();
         }
@@ -168,9 +168,9 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
         /// <param name="updateErrorInfo">The possible <see cref="ErrorInfo"/> that was generated.</param>
         /// <param name="parameter">The user parameter.</param>
         /// <param name="mapper">The mapper to use.</param>
-        public virtual void Update(out ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
+        public virtual void Update(out ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
         {
-            Debug.Assert(IsTypeValid(ref parameter));
+            Debug.Assert(IsTypeValid(in parameter));
 
             updateErrorInfo = new ErrorInfo();
         }

+ 7 - 7
src/Ryujinx.Audio/Renderer/Server/Effect/BiquadFilterEffect.cs

@@ -35,21 +35,21 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
 
         public override EffectType TargetEffectType => EffectType.BiquadFilter;
 
-        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
+        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
         {
-            Update(out updateErrorInfo, ref parameter, mapper);
+            Update(out updateErrorInfo, in parameter, mapper);
         }
 
-        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
+        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
         {
-            Update(out updateErrorInfo, ref parameter, mapper);
+            Update(out updateErrorInfo, in parameter, mapper);
         }
 
-        public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
+        public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
         {
-            Debug.Assert(IsTypeValid(ref parameter));
+            Debug.Assert(IsTypeValid(in parameter));
 
-            UpdateParameterBase(ref parameter);
+            UpdateParameterBase(in parameter);
 
             Parameter = MemoryMarshal.Cast<byte, BiquadFilterEffectParameter>(parameter.SpecificData)[0];
             IsEnabled = parameter.IsEnabled;

+ 7 - 7
src/Ryujinx.Audio/Renderer/Server/Effect/BufferMixEffect.cs

@@ -19,21 +19,21 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
 
         public override EffectType TargetEffectType => EffectType.BufferMix;
 
-        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
+        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
         {
-            Update(out updateErrorInfo, ref parameter, mapper);
+            Update(out updateErrorInfo, in parameter, mapper);
         }
 
-        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
+        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
         {
-            Update(out updateErrorInfo, ref parameter, mapper);
+            Update(out updateErrorInfo, in parameter, mapper);
         }
 
-        public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
+        public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
         {
-            Debug.Assert(IsTypeValid(ref parameter));
+            Debug.Assert(IsTypeValid(in parameter));
 
-            UpdateParameterBase(ref parameter);
+            UpdateParameterBase(in parameter);
 
             Parameter = MemoryMarshal.Cast<byte, BufferMixParameter>(parameter.SpecificData)[0];
             IsEnabled = parameter.IsEnabled;

+ 7 - 7
src/Ryujinx.Audio/Renderer/Server/Effect/CaptureBufferEffect.cs

@@ -32,21 +32,21 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
             return WorkBuffers[index].GetReference(true);
         }
 
-        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
+        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
         {
-            Update(out updateErrorInfo, ref parameter, mapper);
+            Update(out updateErrorInfo, in parameter, mapper);
         }
 
-        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
+        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
         {
-            Update(out updateErrorInfo, ref parameter, mapper);
+            Update(out updateErrorInfo, in parameter, mapper);
         }
 
-        public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
+        public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
         {
-            Debug.Assert(IsTypeValid(ref parameter));
+            Debug.Assert(IsTypeValid(in parameter));
 
-            UpdateParameterBase(ref parameter);
+            UpdateParameterBase(in parameter);
 
             Parameter = MemoryMarshal.Cast<byte, AuxiliaryBufferParameter>(parameter.SpecificData)[0];
             IsEnabled = parameter.IsEnabled;

+ 4 - 4
src/Ryujinx.Audio/Renderer/Server/Effect/CompressorEffect.cs

@@ -39,17 +39,17 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
             return GetSingleBuffer();
         }
 
-        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
+        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
         {
             // Nintendo doesn't do anything here but we still require updateErrorInfo to be initialised.
             updateErrorInfo = new BehaviourParameter.ErrorInfo();
         }
 
-        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
+        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
         {
-            Debug.Assert(IsTypeValid(ref parameter));
+            Debug.Assert(IsTypeValid(in parameter));
 
-            UpdateParameterBase(ref parameter);
+            UpdateParameterBase(in parameter);
 
             Parameter = MemoryMarshal.Cast<byte, CompressorParameter>(parameter.SpecificData)[0];
             IsEnabled = parameter.IsEnabled;

+ 7 - 7
src/Ryujinx.Audio/Renderer/Server/Effect/DelayEffect.cs

@@ -37,19 +37,19 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
             return GetSingleBuffer();
         }
 
-        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
+        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
         {
-            Update(out updateErrorInfo, ref parameter, mapper);
+            Update(out updateErrorInfo, in parameter, mapper);
         }
 
-        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
+        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
         {
-            Update(out updateErrorInfo, ref parameter, mapper);
+            Update(out updateErrorInfo, in parameter, mapper);
         }
 
-        public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
+        public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
         {
-            Debug.Assert(IsTypeValid(ref parameter));
+            Debug.Assert(IsTypeValid(in parameter));
 
             ref DelayParameter delayParameter = ref MemoryMarshal.Cast<byte, DelayParameter>(parameter.SpecificData)[0];
 
@@ -57,7 +57,7 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
 
             if (delayParameter.IsChannelCountMaxValid())
             {
-                UpdateParameterBase(ref parameter);
+                UpdateParameterBase(in parameter);
 
                 UsageState oldParameterStatus = Parameter.Status;
 

+ 7 - 7
src/Ryujinx.Audio/Renderer/Server/Effect/LimiterEffect.cs

@@ -39,25 +39,25 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
             return GetSingleBuffer();
         }
 
-        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
+        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
         {
-            Update(out updateErrorInfo, ref parameter, mapper);
+            Update(out updateErrorInfo, in parameter, mapper);
         }
 
-        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
+        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
         {
-            Update(out updateErrorInfo, ref parameter, mapper);
+            Update(out updateErrorInfo, in parameter, mapper);
         }
 
-        public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
+        public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
         {
-            Debug.Assert(IsTypeValid(ref parameter));
+            Debug.Assert(IsTypeValid(in parameter));
 
             ref LimiterParameter limiterParameter = ref MemoryMarshal.Cast<byte, LimiterParameter>(parameter.SpecificData)[0];
 
             updateErrorInfo = new BehaviourParameter.ErrorInfo();
 
-            UpdateParameterBase(ref parameter);
+            UpdateParameterBase(in parameter);
 
             Parameter = limiterParameter;
 

+ 7 - 7
src/Ryujinx.Audio/Renderer/Server/Effect/Reverb3dEffect.cs

@@ -36,19 +36,19 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
             return GetSingleBuffer();
         }
 
-        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
+        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
         {
-            Update(out updateErrorInfo, ref parameter, mapper);
+            Update(out updateErrorInfo, in parameter, mapper);
         }
 
-        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
+        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
         {
-            Update(out updateErrorInfo, ref parameter, mapper);
+            Update(out updateErrorInfo, in parameter, mapper);
         }
 
-        public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
+        public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
         {
-            Debug.Assert(IsTypeValid(ref parameter));
+            Debug.Assert(IsTypeValid(in parameter));
 
             ref Reverb3dParameter reverbParameter = ref MemoryMarshal.Cast<byte, Reverb3dParameter>(parameter.SpecificData)[0];
 
@@ -56,7 +56,7 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
 
             if (reverbParameter.IsChannelCountMaxValid())
             {
-                UpdateParameterBase(ref parameter);
+                UpdateParameterBase(in parameter);
 
                 UsageState oldParameterStatus = Parameter.ParameterStatus;
 

+ 7 - 7
src/Ryujinx.Audio/Renderer/Server/Effect/ReverbEffect.cs

@@ -39,19 +39,19 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
             return GetSingleBuffer();
         }
 
-        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
+        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
         {
-            Update(out updateErrorInfo, ref parameter, mapper);
+            Update(out updateErrorInfo, in parameter, mapper);
         }
 
-        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
+        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
         {
-            Update(out updateErrorInfo, ref parameter, mapper);
+            Update(out updateErrorInfo, in parameter, mapper);
         }
 
-        public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
+        public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
         {
-            Debug.Assert(IsTypeValid(ref parameter));
+            Debug.Assert(IsTypeValid(in parameter));
 
             ref ReverbParameter reverbParameter = ref MemoryMarshal.Cast<byte, ReverbParameter>(parameter.SpecificData)[0];
 
@@ -59,7 +59,7 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
 
             if (reverbParameter.IsChannelCountMaxValid())
             {
-                UpdateParameterBase(ref parameter);
+                UpdateParameterBase(in parameter);
 
                 UsageState oldParameterStatus = Parameter.Status;
 

+ 1 - 1
src/Ryujinx.Audio/Renderer/Server/MemoryPool/PoolMapper.cs

@@ -249,7 +249,7 @@ namespace Ryujinx.Audio.Renderer.Server.MemoryPool
         /// <param name="inParameter">Input user parameter.</param>
         /// <param name="outStatus">Output user parameter.</param>
         /// <returns>Returns the <see cref="UpdateResult"/> of the operations performed.</returns>
-        public UpdateResult Update(ref MemoryPoolState memoryPool, ref MemoryPoolInParameter inParameter, ref MemoryPoolOutStatus outStatus)
+        public UpdateResult Update(ref MemoryPoolState memoryPool, in MemoryPoolInParameter inParameter, ref MemoryPoolOutStatus outStatus)
         {
             MemoryPoolUserState inputState = inParameter.State;
 

+ 3 - 3
src/Ryujinx.Audio/Renderer/Server/Mix/MixState.cs

@@ -195,7 +195,7 @@ namespace Ryujinx.Audio.Renderer.Server.Mix
         /// <param name="parameter">The input parameter of the mix.</param>
         /// <param name="splitterContext">The splitter context.</param>
         /// <returns>Return true, new connections were done on the adjacency matrix.</returns>
-        private bool UpdateConnection(EdgeMatrix edgeMatrix, ref MixParameter parameter, ref SplitterContext splitterContext)
+        private bool UpdateConnection(EdgeMatrix edgeMatrix, in MixParameter parameter, ref SplitterContext splitterContext)
         {
             bool hasNewConnections;
 
@@ -259,7 +259,7 @@ namespace Ryujinx.Audio.Renderer.Server.Mix
         /// <param name="splitterContext">The splitter context.</param>
         /// <param name="behaviourContext">The behaviour context.</param>
         /// <returns>Return true if the mix was changed.</returns>
-        public bool Update(EdgeMatrix edgeMatrix, ref MixParameter parameter, EffectContext effectContext, SplitterContext splitterContext, BehaviourContext behaviourContext)
+        public bool Update(EdgeMatrix edgeMatrix, in MixParameter parameter, EffectContext effectContext, SplitterContext splitterContext, BehaviourContext behaviourContext)
         {
             bool isDirty;
 
@@ -273,7 +273,7 @@ namespace Ryujinx.Audio.Renderer.Server.Mix
 
             if (behaviourContext.IsSplitterSupported())
             {
-                isDirty = UpdateConnection(edgeMatrix, ref parameter, ref splitterContext);
+                isDirty = UpdateConnection(edgeMatrix, in parameter, ref splitterContext);
             }
             else
             {

+ 4 - 4
src/Ryujinx.Audio/Renderer/Server/Sink/BaseSink.cs

@@ -59,7 +59,7 @@ namespace Ryujinx.Audio.Renderer.Server.Sink
         /// </summary>
         /// <param name="parameter">The user parameter.</param>
         /// <returns>Return true, if the <see cref="SinkType"/> sent by the user match the internal <see cref="SinkType"/>.</returns>
-        public bool IsTypeValid(ref SinkInParameter parameter)
+        public bool IsTypeValid(in SinkInParameter parameter)
         {
             return parameter.Type == TargetSinkType;
         }
@@ -76,7 +76,7 @@ namespace Ryujinx.Audio.Renderer.Server.Sink
         /// Update the internal common parameters from user parameter.
         /// </summary>
         /// <param name="parameter">The user parameter.</param>
-        protected void UpdateStandardParameter(ref SinkInParameter parameter)
+        protected void UpdateStandardParameter(in SinkInParameter parameter)
         {
             if (IsUsed != parameter.IsUsed)
             {
@@ -92,9 +92,9 @@ namespace Ryujinx.Audio.Renderer.Server.Sink
         /// <param name="parameter">The user parameter.</param>
         /// <param name="outStatus">The user output status.</param>
         /// <param name="mapper">The mapper to use.</param>
-        public virtual void Update(out ErrorInfo errorInfo, ref SinkInParameter parameter, ref SinkOutStatus outStatus, PoolMapper mapper)
+        public virtual void Update(out ErrorInfo errorInfo, in SinkInParameter parameter, ref SinkOutStatus outStatus, PoolMapper mapper)
         {
-            Debug.Assert(IsTypeValid(ref parameter));
+            Debug.Assert(IsTypeValid(in parameter));
 
             errorInfo = new ErrorInfo();
         }

+ 3 - 3
src/Ryujinx.Audio/Renderer/Server/Sink/CircularBufferSink.cs

@@ -44,18 +44,18 @@ namespace Ryujinx.Audio.Renderer.Server.Sink
 
         public override SinkType TargetSinkType => SinkType.CircularBuffer;
 
-        public override void Update(out BehaviourParameter.ErrorInfo errorInfo, ref SinkInParameter parameter, ref SinkOutStatus outStatus, PoolMapper mapper)
+        public override void Update(out BehaviourParameter.ErrorInfo errorInfo, in SinkInParameter parameter, ref SinkOutStatus outStatus, PoolMapper mapper)
         {
             errorInfo = new BehaviourParameter.ErrorInfo();
             outStatus = new SinkOutStatus();
 
-            Debug.Assert(IsTypeValid(ref parameter));
+            Debug.Assert(IsTypeValid(in parameter));
 
             ref CircularBufferParameter inputDeviceParameter = ref MemoryMarshal.Cast<byte, CircularBufferParameter>(parameter.SpecificData)[0];
 
             if (parameter.IsUsed != IsUsed || ShouldSkip)
             {
-                UpdateStandardParameter(ref parameter);
+                UpdateStandardParameter(in parameter);
 
                 if (parameter.IsUsed)
                 {

+ 3 - 3
src/Ryujinx.Audio/Renderer/Server/Sink/DeviceSink.cs

@@ -49,15 +49,15 @@ namespace Ryujinx.Audio.Renderer.Server.Sink
 
         public override SinkType TargetSinkType => SinkType.Device;
 
-        public override void Update(out BehaviourParameter.ErrorInfo errorInfo, ref SinkInParameter parameter, ref SinkOutStatus outStatus, PoolMapper mapper)
+        public override void Update(out BehaviourParameter.ErrorInfo errorInfo, in SinkInParameter parameter, ref SinkOutStatus outStatus, PoolMapper mapper)
         {
-            Debug.Assert(IsTypeValid(ref parameter));
+            Debug.Assert(IsTypeValid(in parameter));
 
             ref DeviceParameter inputDeviceParameter = ref MemoryMarshal.Cast<byte, DeviceParameter>(parameter.SpecificData)[0];
 
             if (parameter.IsUsed != IsUsed)
             {
-                UpdateStandardParameter(ref parameter);
+                UpdateStandardParameter(in parameter);
                 Parameter = inputDeviceParameter;
             }
             else

+ 31 - 24
src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterContext.cs

@@ -2,10 +2,11 @@ using Ryujinx.Audio.Renderer.Common;
 using Ryujinx.Audio.Renderer.Parameter;
 using Ryujinx.Audio.Renderer.Utils;
 using Ryujinx.Common;
+using Ryujinx.Common.Extensions;
 using System;
+using System.Buffers;
 using System.Diagnostics;
 using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
 
 namespace Ryujinx.Audio.Renderer.Server.Splitter
 {
@@ -25,7 +26,7 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
         private Memory<SplitterDestination> _splitterDestinations;
 
         /// <summary>
-        /// If set to true, trust the user destination count in <see cref="SplitterState.Update(SplitterContext, ref SplitterInParameter, ReadOnlySpan{byte})"/>.
+        /// If set to true, trust the user destination count in <see cref="SplitterState.Update(SplitterContext, in SplitterInParameter, ref SequenceReader{byte})"/>.
         /// </summary>
         public bool IsBugFixed { get; private set; }
 
@@ -110,7 +111,7 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
         /// </summary>
         /// <param name="splitters">The <see cref="SplitterState"/> storage.</param>
         /// <param name="splitterDestinations">The <see cref="SplitterDestination"/> storage.</param>
-        /// <param name="isBugFixed">If set to true, trust the user destination count in <see cref="SplitterState.Update(SplitterContext, ref SplitterInParameter, ReadOnlySpan{byte})"/>.</param>
+        /// <param name="isBugFixed">If set to true, trust the user destination count in <see cref="SplitterState.Update(SplitterContext, in SplitterInParameter, ref SequenceReader{byte})"/>.</param>
         private void Setup(Memory<SplitterState> splitters, Memory<SplitterDestination> splitterDestinations, bool isBugFixed)
         {
             _splitters = splitters;
@@ -148,11 +149,11 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
         /// </summary>
         /// <param name="inputHeader">The splitter header.</param>
         /// <param name="input">The raw data after the splitter header.</param>
-        private void UpdateState(scoped ref SplitterInParameterHeader inputHeader, ref ReadOnlySpan<byte> input)
+        private void UpdateState(in SplitterInParameterHeader inputHeader, ref SequenceReader<byte> input)
         {
             for (int i = 0; i < inputHeader.SplitterCount; i++)
             {
-                SplitterInParameter parameter = MemoryMarshal.Read<SplitterInParameter>(input);
+                ref readonly SplitterInParameter parameter = ref input.GetRefOrRefToCopy<SplitterInParameter>(out _);
 
                 Debug.Assert(parameter.IsMagicValid());
 
@@ -162,10 +163,16 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
                     {
                         ref SplitterState splitter = ref GetState(parameter.Id);
 
-                        splitter.Update(this, ref parameter, input[Unsafe.SizeOf<SplitterInParameter>()..]);
+                        splitter.Update(this, in parameter, ref input);
                     }
 
-                    input = input[(0x1C + parameter.DestinationCount * 4)..];
+                    // NOTE: there are 12 bytes of unused/unknown data after the destination IDs array.
+                    input.Advance(0xC);
+                }
+                else
+                {
+                    input.Rewind(Unsafe.SizeOf<SplitterInParameter>());
+                    break;
                 }
             }
         }
@@ -175,11 +182,11 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
         /// </summary>
         /// <param name="inputHeader">The splitter header.</param>
         /// <param name="input">The raw data after the splitter header.</param>
-        private void UpdateData(scoped ref SplitterInParameterHeader inputHeader, ref ReadOnlySpan<byte> input)
+        private void UpdateData(in SplitterInParameterHeader inputHeader, ref SequenceReader<byte> input)
         {
             for (int i = 0; i < inputHeader.SplitterDestinationCount; i++)
             {
-                SplitterDestinationInParameter parameter = MemoryMarshal.Read<SplitterDestinationInParameter>(input);
+                ref readonly SplitterDestinationInParameter parameter = ref input.GetRefOrRefToCopy<SplitterDestinationInParameter>(out _);
 
                 Debug.Assert(parameter.IsMagicValid());
 
@@ -191,8 +198,11 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
 
                         destination.Update(parameter);
                     }
-
-                    input = input[Unsafe.SizeOf<SplitterDestinationInParameter>()..];
+                }
+                else
+                {
+                    input.Rewind(Unsafe.SizeOf<SplitterDestinationInParameter>());
+                    break;
                 }
             }
         }
@@ -201,36 +211,33 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
         /// Update splitter from user parameters.
         /// </summary>
         /// <param name="input">The input raw user data.</param>
-        /// <param name="consumedSize">The total consumed size.</param>
         /// <returns>Return true if the update was successful.</returns>
-        public bool Update(ReadOnlySpan<byte> input, out int consumedSize)
+        public bool Update(ref SequenceReader<byte> input)
         {
             if (_splitterDestinations.IsEmpty || _splitters.IsEmpty)
             {
-                consumedSize = 0;
-
                 return true;
             }
 
-            int originalSize = input.Length;
-
-            SplitterInParameterHeader header = SpanIOHelper.Read<SplitterInParameterHeader>(ref input);
+            ref readonly SplitterInParameterHeader header = ref input.GetRefOrRefToCopy<SplitterInParameterHeader>(out _);
 
             if (header.IsMagicValid())
             {
                 ClearAllNewConnectionFlag();
 
-                UpdateState(ref header, ref input);
-                UpdateData(ref header, ref input);
+                UpdateState(in header, ref input);
+                UpdateData(in header, ref input);
 
-                consumedSize = BitUtils.AlignUp(originalSize - input.Length, 0x10);
+                input.SetConsumed(BitUtils.AlignUp(input.Consumed, 0x10));
 
                 return true;
             }
+            else
+            {
+                input.Rewind(Unsafe.SizeOf<SplitterInParameterHeader>());
 
-            consumedSize = 0;
-
-            return false;
+                return false;
+            }
         }
 
         /// <summary>

+ 7 - 4
src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterState.cs

@@ -1,4 +1,5 @@
 using Ryujinx.Audio.Renderer.Parameter;
+using Ryujinx.Common.Extensions;
 using System;
 using System.Buffers;
 using System.Diagnostics;
@@ -122,7 +123,7 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
         /// <param name="context">The splitter context.</param>
         /// <param name="parameter">The user parameter.</param>
         /// <param name="input">The raw input data after the <paramref name="parameter"/>.</param>
-        public void Update(SplitterContext context, ref SplitterInParameter parameter, ReadOnlySpan<byte> input)
+        public void Update(SplitterContext context, in SplitterInParameter parameter, ref SequenceReader<byte> input)
         {
             ClearLinks();
 
@@ -139,9 +140,9 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
 
             if (destinationCount > 0)
             {
-                ReadOnlySpan<int> destinationIds = MemoryMarshal.Cast<byte, int>(input);
+                input.ReadLittleEndian(out int destinationId);
 
-                Memory<SplitterDestination> destination = context.GetDestinationMemory(destinationIds[0]);
+                Memory<SplitterDestination> destination = context.GetDestinationMemory(destinationId);
 
                 SetDestination(ref destination.Span[0]);
 
@@ -149,7 +150,9 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
 
                 for (int i = 1; i < destinationCount; i++)
                 {
-                    Memory<SplitterDestination> nextDestination = context.GetDestinationMemory(destinationIds[i]);
+                    input.ReadLittleEndian(out destinationId);
+
+                    Memory<SplitterDestination> nextDestination = context.GetDestinationMemory(destinationId);
 
                     destination.Span[0].Link(ref nextDestination.Span[0]);
                     destination = nextDestination;

+ 71 - 83
src/Ryujinx.Audio/Renderer/Server/StateUpdater.cs

@@ -9,41 +9,40 @@ using Ryujinx.Audio.Renderer.Server.Sink;
 using Ryujinx.Audio.Renderer.Server.Splitter;
 using Ryujinx.Audio.Renderer.Server.Voice;
 using Ryujinx.Audio.Renderer.Utils;
+using Ryujinx.Common.Extensions;
 using Ryujinx.Common.Logging;
 using System;
 using System.Buffers;
 using System.Diagnostics;
 using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
 using static Ryujinx.Audio.Renderer.Common.BehaviourParameter;
 
 namespace Ryujinx.Audio.Renderer.Server
 {
-    public class StateUpdater
+    public ref struct StateUpdater
     {
-        private readonly ReadOnlyMemory<byte> _inputOrigin;
+        private SequenceReader<byte> _inputReader;
+
         private readonly ReadOnlyMemory<byte> _outputOrigin;
-        private ReadOnlyMemory<byte> _input;
 
         private Memory<byte> _output;
         private readonly uint _processHandle;
         private BehaviourContext _behaviourContext;
 
-        private UpdateDataHeader _inputHeader;
+        private readonly ref readonly UpdateDataHeader _inputHeader;
         private readonly Memory<UpdateDataHeader> _outputHeader;
 
-        private ref UpdateDataHeader OutputHeader => ref _outputHeader.Span[0];
+        private readonly ref UpdateDataHeader OutputHeader => ref _outputHeader.Span[0];
 
-        public StateUpdater(ReadOnlyMemory<byte> input, Memory<byte> output, uint processHandle, BehaviourContext behaviourContext)
+        public StateUpdater(ReadOnlySequence<byte> input, Memory<byte> output, uint processHandle, BehaviourContext behaviourContext)
         {
-            _input = input;
-            _inputOrigin = _input;
+            _inputReader = new SequenceReader<byte>(input);
             _output = output;
             _outputOrigin = _output;
             _processHandle = processHandle;
             _behaviourContext = behaviourContext;
 
-            _inputHeader = SpanIOHelper.Read<UpdateDataHeader>(ref _input);
+            _inputHeader = ref _inputReader.GetRefOrRefToCopy<UpdateDataHeader>(out _);
 
             _outputHeader = SpanMemoryManager<UpdateDataHeader>.Cast(_output[..Unsafe.SizeOf<UpdateDataHeader>()]);
             OutputHeader.Initialize(_behaviourContext.UserRevision);
@@ -52,7 +51,7 @@ namespace Ryujinx.Audio.Renderer.Server
 
         public ResultCode UpdateBehaviourContext()
         {
-            BehaviourParameter parameter = SpanIOHelper.Read<BehaviourParameter>(ref _input);
+            ref readonly BehaviourParameter parameter = ref _inputReader.GetRefOrRefToCopy<BehaviourParameter>(out _);
 
             if (!BehaviourContext.CheckValidRevision(parameter.UserRevision) || parameter.UserRevision != _behaviourContext.UserRevision)
             {
@@ -81,11 +80,11 @@ namespace Ryujinx.Audio.Renderer.Server
 
             foreach (ref MemoryPoolState memoryPool in memoryPools)
             {
-                MemoryPoolInParameter parameter = SpanIOHelper.Read<MemoryPoolInParameter>(ref _input);
+                ref readonly MemoryPoolInParameter parameter = ref _inputReader.GetRefOrRefToCopy<MemoryPoolInParameter>(out _);
 
                 ref MemoryPoolOutStatus outStatus = ref SpanIOHelper.GetWriteRef<MemoryPoolOutStatus>(ref _output)[0];
 
-                PoolMapper.UpdateResult updateResult = mapper.Update(ref memoryPool, ref parameter, ref outStatus);
+                PoolMapper.UpdateResult updateResult = mapper.Update(ref memoryPool, in parameter, ref outStatus);
 
                 if (updateResult != PoolMapper.UpdateResult.Success &&
                     updateResult != PoolMapper.UpdateResult.MapError &&
@@ -115,7 +114,7 @@ namespace Ryujinx.Audio.Renderer.Server
 
             for (int i = 0; i < context.GetCount(); i++)
             {
-                VoiceChannelResourceInParameter parameter = SpanIOHelper.Read<VoiceChannelResourceInParameter>(ref _input);
+                ref readonly VoiceChannelResourceInParameter parameter = ref _inputReader.GetRefOrRefToCopy<VoiceChannelResourceInParameter>(out _);
 
                 ref VoiceChannelResource resource = ref context.GetChannelResource(i);
 
@@ -127,7 +126,7 @@ namespace Ryujinx.Audio.Renderer.Server
             return ResultCode.Success;
         }
 
-        public ResultCode UpdateVoices(VoiceContext context, Memory<MemoryPoolState> memoryPools)
+        public ResultCode UpdateVoices(VoiceContext context, PoolMapper mapper)
         {
             if (context.GetCount() * Unsafe.SizeOf<VoiceInParameter>() != _inputHeader.VoicesSize)
             {
@@ -136,11 +135,7 @@ namespace Ryujinx.Audio.Renderer.Server
 
             int initialOutputSize = _output.Length;
 
-            ReadOnlySpan<VoiceInParameter> parameters = MemoryMarshal.Cast<byte, VoiceInParameter>(_input[..(int)_inputHeader.VoicesSize].Span);
-
-            _input = _input[(int)_inputHeader.VoicesSize..];
-
-            PoolMapper mapper = new(_processHandle, memoryPools, _behaviourContext.IsMemoryPoolForceMappingEnabled());
+            long initialInputConsumed = _inputReader.Consumed;
 
             // First make everything not in use.
             for (int i = 0; i < context.GetCount(); i++)
@@ -157,7 +152,7 @@ namespace Ryujinx.Audio.Renderer.Server
             // Start processing
             for (int i = 0; i < context.GetCount(); i++)
             {
-                VoiceInParameter parameter = parameters[i];
+                ref readonly VoiceInParameter parameter = ref _inputReader.GetRefOrRefToCopy<VoiceInParameter>(out _);
 
                 voiceUpdateStates.Fill(Memory<VoiceUpdateState>.Empty);
 
@@ -181,14 +176,14 @@ namespace Ryujinx.Audio.Renderer.Server
                         currentVoiceState.Initialize();
                     }
 
-                    currentVoiceState.UpdateParameters(out ErrorInfo updateParameterError, ref parameter, ref mapper, ref _behaviourContext);
+                    currentVoiceState.UpdateParameters(out ErrorInfo updateParameterError, in parameter, mapper, ref _behaviourContext);
 
                     if (updateParameterError.ErrorCode != ResultCode.Success)
                     {
                         _behaviourContext.AppendError(ref updateParameterError);
                     }
 
-                    currentVoiceState.UpdateWaveBuffers(out ErrorInfo[] waveBufferUpdateErrorInfos, ref parameter, voiceUpdateStates, ref mapper, ref _behaviourContext);
+                    currentVoiceState.UpdateWaveBuffers(out ErrorInfo[] waveBufferUpdateErrorInfos, in parameter, voiceUpdateStates, mapper, ref _behaviourContext);
 
                     foreach (ref ErrorInfo errorInfo in waveBufferUpdateErrorInfos.AsSpan())
                     {
@@ -198,7 +193,7 @@ namespace Ryujinx.Audio.Renderer.Server
                         }
                     }
 
-                    currentVoiceState.WriteOutStatus(ref outStatus, ref parameter, voiceUpdateStates);
+                    currentVoiceState.WriteOutStatus(ref outStatus, in parameter, voiceUpdateStates);
                 }
             }
 
@@ -211,10 +206,12 @@ namespace Ryujinx.Audio.Renderer.Server
 
             Debug.Assert((initialOutputSize - currentOutputSize) == OutputHeader.VoicesSize);
 
+            _inputReader.SetConsumed(initialInputConsumed + _inputHeader.VoicesSize);
+
             return ResultCode.Success;
         }
 
-        private static void ResetEffect<T>(ref BaseEffect effect, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
+        private static void ResetEffect<T>(ref BaseEffect effect, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
         {
             effect.ForceUnmapBuffers(mapper);
 
@@ -234,17 +231,17 @@ namespace Ryujinx.Audio.Renderer.Server
             };
         }
 
-        public ResultCode UpdateEffects(EffectContext context, bool isAudioRendererActive, Memory<MemoryPoolState> memoryPools)
+        public ResultCode UpdateEffects(EffectContext context, bool isAudioRendererActive, PoolMapper mapper)
         {
             if (_behaviourContext.IsEffectInfoVersion2Supported())
             {
-                return UpdateEffectsVersion2(context, isAudioRendererActive, memoryPools);
+                return UpdateEffectsVersion2(context, isAudioRendererActive, mapper);
             }
 
-            return UpdateEffectsVersion1(context, isAudioRendererActive, memoryPools);
+            return UpdateEffectsVersion1(context, isAudioRendererActive, mapper);
         }
 
-        public ResultCode UpdateEffectsVersion2(EffectContext context, bool isAudioRendererActive, Memory<MemoryPoolState> memoryPools)
+        public ResultCode UpdateEffectsVersion2(EffectContext context, bool isAudioRendererActive, PoolMapper mapper)
         {
             if (context.GetCount() * Unsafe.SizeOf<EffectInParameterVersion2>() != _inputHeader.EffectsSize)
             {
@@ -253,26 +250,22 @@ namespace Ryujinx.Audio.Renderer.Server
 
             int initialOutputSize = _output.Length;
 
-            ReadOnlySpan<EffectInParameterVersion2> parameters = MemoryMarshal.Cast<byte, EffectInParameterVersion2>(_input[..(int)_inputHeader.EffectsSize].Span);
-
-            _input = _input[(int)_inputHeader.EffectsSize..];
-
-            PoolMapper mapper = new(_processHandle, memoryPools, _behaviourContext.IsMemoryPoolForceMappingEnabled());
+            long initialInputConsumed = _inputReader.Consumed;
 
             for (int i = 0; i < context.GetCount(); i++)
             {
-                EffectInParameterVersion2 parameter = parameters[i];
+                ref readonly EffectInParameterVersion2 parameter = ref _inputReader.GetRefOrRefToCopy<EffectInParameterVersion2>(out _);
 
                 ref EffectOutStatusVersion2 outStatus = ref SpanIOHelper.GetWriteRef<EffectOutStatusVersion2>(ref _output)[0];
 
                 ref BaseEffect effect = ref context.GetEffect(i);
 
-                if (!effect.IsTypeValid(ref parameter))
+                if (!effect.IsTypeValid(in parameter))
                 {
-                    ResetEffect(ref effect, ref parameter, mapper);
+                    ResetEffect(ref effect, in parameter, mapper);
                 }
 
-                effect.Update(out ErrorInfo updateErrorInfo, ref parameter, mapper);
+                effect.Update(out ErrorInfo updateErrorInfo, in parameter, mapper);
 
                 if (updateErrorInfo.ErrorCode != ResultCode.Success)
                 {
@@ -297,10 +290,12 @@ namespace Ryujinx.Audio.Renderer.Server
 
             Debug.Assert((initialOutputSize - currentOutputSize) == OutputHeader.EffectsSize);
 
+            _inputReader.SetConsumed(initialInputConsumed + _inputHeader.EffectsSize);
+
             return ResultCode.Success;
         }
 
-        public ResultCode UpdateEffectsVersion1(EffectContext context, bool isAudioRendererActive, Memory<MemoryPoolState> memoryPools)
+        public ResultCode UpdateEffectsVersion1(EffectContext context, bool isAudioRendererActive, PoolMapper mapper)
         {
             if (context.GetCount() * Unsafe.SizeOf<EffectInParameterVersion1>() != _inputHeader.EffectsSize)
             {
@@ -309,26 +304,22 @@ namespace Ryujinx.Audio.Renderer.Server
 
             int initialOutputSize = _output.Length;
 
-            ReadOnlySpan<EffectInParameterVersion1> parameters = MemoryMarshal.Cast<byte, EffectInParameterVersion1>(_input[..(int)_inputHeader.EffectsSize].Span);
-
-            _input = _input[(int)_inputHeader.EffectsSize..];
-
-            PoolMapper mapper = new(_processHandle, memoryPools, _behaviourContext.IsMemoryPoolForceMappingEnabled());
+            long initialInputConsumed = _inputReader.Consumed;
 
             for (int i = 0; i < context.GetCount(); i++)
             {
-                EffectInParameterVersion1 parameter = parameters[i];
+                ref readonly EffectInParameterVersion1 parameter = ref _inputReader.GetRefOrRefToCopy<EffectInParameterVersion1>(out _);
 
                 ref EffectOutStatusVersion1 outStatus = ref SpanIOHelper.GetWriteRef<EffectOutStatusVersion1>(ref _output)[0];
 
                 ref BaseEffect effect = ref context.GetEffect(i);
 
-                if (!effect.IsTypeValid(ref parameter))
+                if (!effect.IsTypeValid(in parameter))
                 {
-                    ResetEffect(ref effect, ref parameter, mapper);
+                    ResetEffect(ref effect, in parameter, mapper);
                 }
 
-                effect.Update(out ErrorInfo updateErrorInfo, ref parameter, mapper);
+                effect.Update(out ErrorInfo updateErrorInfo, in parameter, mapper);
 
                 if (updateErrorInfo.ErrorCode != ResultCode.Success)
                 {
@@ -345,38 +336,40 @@ namespace Ryujinx.Audio.Renderer.Server
 
             Debug.Assert((initialOutputSize - currentOutputSize) == OutputHeader.EffectsSize);
 
+            _inputReader.SetConsumed(initialInputConsumed + _inputHeader.EffectsSize);
+
             return ResultCode.Success;
         }
 
         public ResultCode UpdateSplitter(SplitterContext context)
         {
-            if (context.Update(_input.Span, out int consumedSize))
+            if (context.Update(ref _inputReader))
             {
-                _input = _input[consumedSize..];
-
                 return ResultCode.Success;
             }
 
             return ResultCode.InvalidUpdateInfo;
         }
 
-        private static bool CheckMixParametersValidity(MixContext mixContext, uint mixBufferCount, uint inputMixCount, ReadOnlySpan<MixParameter> parameters)
+        private static bool CheckMixParametersValidity(MixContext mixContext, uint mixBufferCount, uint inputMixCount, SequenceReader<byte> parameters)
         {
             uint maxMixStateCount = mixContext.GetCount();
             uint totalRequiredMixBufferCount = 0;
 
             for (int i = 0; i < inputMixCount; i++)
             {
-                if (parameters[i].IsUsed)
+                ref readonly MixParameter parameter = ref parameters.GetRefOrRefToCopy<MixParameter>(out _);
+
+                if (parameter.IsUsed)
                 {
-                    if (parameters[i].DestinationMixId != Constants.UnusedMixId &&
-                        parameters[i].DestinationMixId > maxMixStateCount &&
-                        parameters[i].MixId != Constants.FinalMixId)
+                    if (parameter.DestinationMixId != Constants.UnusedMixId &&
+                        parameter.DestinationMixId > maxMixStateCount &&
+                        parameter.MixId != Constants.FinalMixId)
                     {
                         return true;
                     }
 
-                    totalRequiredMixBufferCount += parameters[i].BufferCount;
+                    totalRequiredMixBufferCount += parameter.BufferCount;
                 }
             }
 
@@ -391,7 +384,7 @@ namespace Ryujinx.Audio.Renderer.Server
 
             if (_behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported())
             {
-                MixInParameterDirtyOnlyUpdate parameter = MemoryMarshal.Cast<byte, MixInParameterDirtyOnlyUpdate>(_input.Span)[0];
+                ref readonly MixInParameterDirtyOnlyUpdate parameter = ref _inputReader.GetRefOrRefToCopy<MixInParameterDirtyOnlyUpdate>(out _);
 
                 mixCount = parameter.MixCount;
 
@@ -411,25 +404,20 @@ namespace Ryujinx.Audio.Renderer.Server
                 return ResultCode.InvalidUpdateInfo;
             }
 
-            if (_behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported())
-            {
-                _input = _input[Unsafe.SizeOf<MixInParameterDirtyOnlyUpdate>()..];
-            }
-
-            ReadOnlySpan<MixParameter> parameters = MemoryMarshal.Cast<byte, MixParameter>(_input.Span[..(int)inputMixSize]);
+            long initialInputConsumed = _inputReader.Consumed;
 
-            _input = _input[(int)inputMixSize..];
+            int parameterCount = (int)inputMixSize / Unsafe.SizeOf<MixParameter>();
 
-            if (CheckMixParametersValidity(mixContext, mixBufferCount, mixCount, parameters))
+            if (CheckMixParametersValidity(mixContext, mixBufferCount, mixCount, _inputReader))
             {
                 return ResultCode.InvalidUpdateInfo;
             }
 
             bool isMixContextDirty = false;
 
-            for (int i = 0; i < parameters.Length; i++)
+            for (int i = 0; i < parameterCount; i++)
             {
-                MixParameter parameter = parameters[i];
+                ref readonly MixParameter parameter = ref _inputReader.GetRefOrRefToCopy<MixParameter>(out _);
 
                 int mixId = i;
 
@@ -454,7 +442,7 @@ namespace Ryujinx.Audio.Renderer.Server
 
                 if (mix.IsUsed)
                 {
-                    isMixContextDirty |= mix.Update(mixContext.EdgeMatrix, ref parameter, effectContext, splitterContext, _behaviourContext);
+                    isMixContextDirty |= mix.Update(mixContext.EdgeMatrix, in parameter, effectContext, splitterContext, _behaviourContext);
                 }
             }
 
@@ -473,10 +461,12 @@ namespace Ryujinx.Audio.Renderer.Server
                 }
             }
 
+            _inputReader.SetConsumed(initialInputConsumed + inputMixSize);
+
             return ResultCode.Success;
         }
 
-        private static void ResetSink(ref BaseSink sink, ref SinkInParameter parameter)
+        private static void ResetSink(ref BaseSink sink, in SinkInParameter parameter)
         {
             sink.CleanUp();
 
@@ -489,10 +479,8 @@ namespace Ryujinx.Audio.Renderer.Server
             };
         }
 
-        public ResultCode UpdateSinks(SinkContext context, Memory<MemoryPoolState> memoryPools)
+        public ResultCode UpdateSinks(SinkContext context, PoolMapper mapper)
         {
-            PoolMapper mapper = new(_processHandle, memoryPools, _behaviourContext.IsMemoryPoolForceMappingEnabled());
-
             if (context.GetCount() * Unsafe.SizeOf<SinkInParameter>() != _inputHeader.SinksSize)
             {
                 return ResultCode.InvalidUpdateInfo;
@@ -500,22 +488,20 @@ namespace Ryujinx.Audio.Renderer.Server
 
             int initialOutputSize = _output.Length;
 
-            ReadOnlySpan<SinkInParameter> parameters = MemoryMarshal.Cast<byte, SinkInParameter>(_input[..(int)_inputHeader.SinksSize].Span);
-
-            _input = _input[(int)_inputHeader.SinksSize..];
+            long initialInputConsumed = _inputReader.Consumed;
 
             for (int i = 0; i < context.GetCount(); i++)
             {
-                SinkInParameter parameter = parameters[i];
+                ref readonly SinkInParameter parameter = ref _inputReader.GetRefOrRefToCopy<SinkInParameter>(out _);
                 ref SinkOutStatus outStatus = ref SpanIOHelper.GetWriteRef<SinkOutStatus>(ref _output)[0];
                 ref BaseSink sink = ref context.GetSink(i);
 
-                if (!sink.IsTypeValid(ref parameter))
+                if (!sink.IsTypeValid(in parameter))
                 {
-                    ResetSink(ref sink, ref parameter);
+                    ResetSink(ref sink, in parameter);
                 }
 
-                sink.Update(out ErrorInfo updateErrorInfo, ref parameter, ref outStatus, mapper);
+                sink.Update(out ErrorInfo updateErrorInfo, in parameter, ref outStatus, mapper);
 
                 if (updateErrorInfo.ErrorCode != ResultCode.Success)
                 {
@@ -530,6 +516,8 @@ namespace Ryujinx.Audio.Renderer.Server
 
             Debug.Assert((initialOutputSize - currentOutputSize) == OutputHeader.SinksSize);
 
+            _inputReader.SetConsumed(initialInputConsumed + _inputHeader.SinksSize);
+
             return ResultCode.Success;
         }
 
@@ -540,7 +528,7 @@ namespace Ryujinx.Audio.Renderer.Server
                 return ResultCode.InvalidUpdateInfo;
             }
 
-            PerformanceInParameter parameter = SpanIOHelper.Read<PerformanceInParameter>(ref _input);
+            ref readonly PerformanceInParameter parameter = ref _inputReader.GetRefOrRefToCopy<PerformanceInParameter>(out _);
 
             ref PerformanceOutStatus outStatus = ref SpanIOHelper.GetWriteRef<PerformanceOutStatus>(ref _output)[0];
 
@@ -585,9 +573,9 @@ namespace Ryujinx.Audio.Renderer.Server
             return ResultCode.Success;
         }
 
-        public ResultCode CheckConsumedSize()
+        public readonly ResultCode CheckConsumedSize()
         {
-            int consumedInputSize = _inputOrigin.Length - _input.Length;
+            long consumedInputSize = _inputReader.Consumed;
             int consumedOutputSize = _outputOrigin.Length - _output.Length;
 
             if (consumedInputSize != _inputHeader.TotalSize)

+ 19 - 7
src/Ryujinx.Audio/Renderer/Server/Voice/VoiceState.cs

@@ -254,7 +254,7 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
         /// </summary>
         /// <param name="parameter">The user parameter.</param>
         /// <returns>Return true, if the server voice information needs to be updated.</returns>
-        private readonly bool ShouldUpdateParameters(ref VoiceInParameter parameter)
+        private readonly bool ShouldUpdateParameters(in VoiceInParameter parameter)
         {
             if (DataSourceStateAddressInfo.CpuAddress == parameter.DataSourceStateAddress)
             {
@@ -273,7 +273,7 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
         /// <param name="parameter">The user parameter.</param>
         /// <param name="poolMapper">The mapper to use.</param>
         /// <param name="behaviourContext">The behaviour context.</param>
-        public void UpdateParameters(out ErrorInfo outErrorInfo, ref VoiceInParameter parameter, ref PoolMapper poolMapper, ref BehaviourContext behaviourContext)
+        public void UpdateParameters(out ErrorInfo outErrorInfo, in VoiceInParameter parameter, PoolMapper poolMapper, ref BehaviourContext behaviourContext)
         {
             InUse = parameter.InUse;
             Id = parameter.Id;
@@ -326,7 +326,7 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
                 VoiceDropFlag = false;
             }
 
-            if (ShouldUpdateParameters(ref parameter))
+            if (ShouldUpdateParameters(in parameter))
             {
                 DataSourceStateUnmapped = !poolMapper.TryAttachBuffer(out outErrorInfo, ref DataSourceStateAddressInfo, parameter.DataSourceStateAddress, parameter.DataSourceStateSize);
             }
@@ -380,7 +380,7 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
         /// <param name="outStatus">The given user output.</param>
         /// <param name="parameter">The user parameter.</param>
         /// <param name="voiceUpdateStates">The voice states associated to the <see cref="VoiceState"/>.</param>
-        public void WriteOutStatus(ref VoiceOutStatus outStatus, ref VoiceInParameter parameter, ReadOnlySpan<Memory<VoiceUpdateState>> voiceUpdateStates)
+        public void WriteOutStatus(ref VoiceOutStatus outStatus, in VoiceInParameter parameter, ReadOnlySpan<Memory<VoiceUpdateState>> voiceUpdateStates)
         {
 #if DEBUG
             // Sanity check in debug mode of the internal state
@@ -426,7 +426,12 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
         /// <param name="voiceUpdateStates">The voice states associated to the <see cref="VoiceState"/>.</param>
         /// <param name="mapper">The mapper to use.</param>
         /// <param name="behaviourContext">The behaviour context.</param>
-        public void UpdateWaveBuffers(out ErrorInfo[] errorInfos, ref VoiceInParameter parameter, ReadOnlySpan<Memory<VoiceUpdateState>> voiceUpdateStates, ref PoolMapper mapper, ref BehaviourContext behaviourContext)
+        public void UpdateWaveBuffers(
+            out ErrorInfo[] errorInfos,
+            in VoiceInParameter parameter,
+            ReadOnlySpan<Memory<VoiceUpdateState>> voiceUpdateStates,
+            PoolMapper mapper,
+            ref BehaviourContext behaviourContext)
         {
             errorInfos = new ErrorInfo[Constants.VoiceWaveBufferCount * 2];
 
@@ -444,7 +449,7 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
 
             for (int i = 0; i < Constants.VoiceWaveBufferCount; i++)
             {
-                UpdateWaveBuffer(errorInfos.AsSpan(i * 2, 2), ref WaveBuffers[i], ref parameter.WaveBuffers[i], parameter.SampleFormat, voiceUpdateState.IsWaveBufferValid[i], ref mapper, ref behaviourContext);
+                UpdateWaveBuffer(errorInfos.AsSpan(i * 2, 2), ref WaveBuffers[i], ref parameter.WaveBuffers[i], parameter.SampleFormat, voiceUpdateState.IsWaveBufferValid[i], mapper, ref behaviourContext);
             }
         }
 
@@ -458,7 +463,14 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
         /// <param name="isValid">If set to true, the server side wavebuffer is considered valid.</param>
         /// <param name="mapper">The mapper to use.</param>
         /// <param name="behaviourContext">The behaviour context.</param>
-        private void UpdateWaveBuffer(Span<ErrorInfo> errorInfos, ref WaveBuffer waveBuffer, ref WaveBufferInternal inputWaveBuffer, SampleFormat sampleFormat, bool isValid, ref PoolMapper mapper, ref BehaviourContext behaviourContext)
+        private void UpdateWaveBuffer(
+            Span<ErrorInfo> errorInfos,
+            ref WaveBuffer waveBuffer,
+            ref WaveBufferInternal inputWaveBuffer,
+            SampleFormat sampleFormat,
+            bool isValid,
+            PoolMapper mapper,
+            ref BehaviourContext behaviourContext)
         {
             if (!isValid && waveBuffer.IsSendToAudioProcessor && waveBuffer.BufferAddressInfo.CpuAddress != 0)
             {

+ 181 - 0
src/Ryujinx.Common/Extensions/SequenceReaderExtensions.cs

@@ -0,0 +1,181 @@
+using System;
+using System.Buffers;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Common.Extensions
+{
+    public static class SequenceReaderExtensions
+    {
+        /// <summary>
+        /// Dumps the entire <see cref="SequenceReader{byte}"/> to a file, restoring its previous location afterward.
+        /// Useful for debugging purposes.
+        /// </summary>
+        /// <param name="reader">The <see cref="SequenceReader{Byte}"/> to write to a file</param>
+        /// <param name="fileFullName">The path and name of the file to create and dump to</param>
+        public static void DumpToFile(this ref SequenceReader<byte> reader, string fileFullName)
+        {
+            var initialConsumed = reader.Consumed;
+
+            reader.Rewind(initialConsumed);
+
+            using (var fileStream = System.IO.File.Create(fileFullName, 4096, System.IO.FileOptions.None))
+            {
+                while (reader.End == false)
+                {
+                    var span = reader.CurrentSpan;
+                    fileStream.Write(span);
+                    reader.Advance(span.Length);
+                }
+            }
+
+            reader.SetConsumed(initialConsumed);
+        }
+
+        /// <summary>
+        /// Returns a reference to the desired value. This ref should always be used. The argument passed in <paramref name="copyDestinationIfRequiredDoNotUse"/> should never be used, as this is only used for storage if the value
+        /// must be copied from multiple <see cref="ReadOnlyMemory{Byte}"/> segments held by the <see cref="SequenceReader{Byte}"/>.
+        /// </summary>
+        /// <typeparam name="T">Type to get</typeparam>
+        /// <param name="reader">The <see cref="SequenceReader{Byte}"/> to read from</param>
+        /// <param name="copyDestinationIfRequiredDoNotUse">A location used as storage if (and only if) the value to be read spans multiple <see cref="ReadOnlyMemory{Byte}"/> segments</param>
+        /// <returns>A reference to the desired value, either directly to memory in the <see cref="SequenceReader{Byte}"/>, or to <paramref name="copyDestinationIfRequiredDoNotUse"/> if it has been used for copying the value in to</returns>
+        /// <remarks>
+        /// DO NOT use <paramref name="copyDestinationIfRequiredDoNotUse"/> after calling this method, as it will only
+        /// contain a value if the value couldn't be referenced directly because it spans multiple <see cref="ReadOnlyMemory{Byte}"/> segments.
+        /// To discourage use, it is recommended to to call this method like the following:
+        /// <c>
+        ///     ref readonly MyStruct value = ref sequenceReader.GetRefOrRefToCopy{MyStruct}(out _);
+        /// </c>
+        /// </remarks>
+        /// <exception cref="ArgumentOutOfRangeException">The <see cref="SequenceReader{Byte}"/> does not contain enough data to read a value of type <typeparamref name="T"/></exception>
+        public static ref readonly T GetRefOrRefToCopy<T>(this scoped ref SequenceReader<byte> reader, out T copyDestinationIfRequiredDoNotUse) where T : unmanaged
+        {
+            int lengthRequired = Unsafe.SizeOf<T>();
+
+            ReadOnlySpan<byte> span = reader.UnreadSpan;
+            if (lengthRequired <= span.Length)
+            {
+                reader.Advance(lengthRequired);
+
+                copyDestinationIfRequiredDoNotUse = default;
+
+                ReadOnlySpan<T> spanOfT = MemoryMarshal.Cast<byte, T>(span);
+
+                return ref spanOfT[0];
+            }
+            else
+            {
+                copyDestinationIfRequiredDoNotUse = default;
+
+                Span<T> valueSpan = MemoryMarshal.CreateSpan(ref copyDestinationIfRequiredDoNotUse, 1);
+
+                Span<byte> valueBytesSpan = MemoryMarshal.AsBytes(valueSpan);
+
+                if (!reader.TryCopyTo(valueBytesSpan))
+                {
+                    throw new ArgumentOutOfRangeException(nameof(reader), "The sequence is not long enough to read the desired value.");
+                }
+
+                reader.Advance(lengthRequired);
+
+                return ref valueSpan[0];
+            }
+        }
+
+        /// <summary>
+        /// Reads an <see cref="int"/> as little endian.
+        /// </summary>
+        /// <param name="reader">The <see cref="SequenceReader{Byte}"/> to read from</param>
+        /// <param name="value">A location to receive the read value</param>
+        /// <exception cref="ArgumentOutOfRangeException">Thrown if there wasn't enough data for an <see cref="int"/></exception>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static void ReadLittleEndian(this ref SequenceReader<byte> reader, out int value)
+        {
+            if (!reader.TryReadLittleEndian(out value))
+            {
+                throw new ArgumentOutOfRangeException(nameof(value), "The sequence is not long enough to read the desired value.");
+            }
+        }
+
+        /// <summary>
+        /// Reads the desired unmanaged value by copying it to the specified <paramref name="value"/>.
+        /// </summary>
+        /// <typeparam name="T">Type to read</typeparam>
+        /// <param name="reader">The <see cref="SequenceReader{Byte}"/> to read from</param>
+        /// <param name="value">The target that will receive the read value</param>
+        /// <exception cref="ArgumentOutOfRangeException">The <see cref="SequenceReader{Byte}"/> does not contain enough data to read a value of type <typeparamref name="T"/></exception>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static void ReadUnmanaged<T>(this ref SequenceReader<byte> reader, out T value) where T : unmanaged
+        {
+            if (!reader.TryReadUnmanaged(out value))
+            {
+                throw new ArgumentOutOfRangeException(nameof(value), "The sequence is not long enough to read the desired value.");
+            }
+        }
+
+        /// <summary>
+        /// Sets the reader's position as bytes consumed.
+        /// </summary>
+        /// <param name="reader">The <see cref="SequenceReader{Byte}"/> to set the position</param>
+        /// <param name="consumed">The number of bytes consumed</param>
+        public static void SetConsumed(ref this SequenceReader<byte> reader, long consumed)
+        {
+            reader.Rewind(reader.Consumed);
+            reader.Advance(consumed);
+        }
+
+        /// <summary>
+        /// Try to read the given type out of the buffer if possible. Warning: this is dangerous to use with arbitrary
+        /// structs - see remarks for full details.
+        /// </summary>
+        /// <typeparam name="T">Type to read</typeparam>
+        /// <remarks>
+        /// IMPORTANT: The read is a straight copy of bits. If a struct depends on specific state of it's members to
+        /// behave correctly this can lead to exceptions, etc. If reading endian specific integers, use the explicit
+        /// overloads such as <see cref="SequenceReader{T}.TryReadLittleEndian"/>
+        /// </remarks>
+        /// <returns>
+        /// True if successful. <paramref name="value"/> will be default if failed (due to lack of space).
+        /// </returns>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static unsafe bool TryReadUnmanaged<T>(ref this SequenceReader<byte> reader, out T value) where T : unmanaged
+        {
+            ReadOnlySpan<byte> span = reader.UnreadSpan;
+
+            if (span.Length < sizeof(T))
+            {
+                return TryReadUnmanagedMultiSegment(ref reader, out value);
+            }
+
+            value = Unsafe.ReadUnaligned<T>(ref MemoryMarshal.GetReference(span));
+
+            reader.Advance(sizeof(T));
+
+            return true;
+        }
+
+        private static unsafe bool TryReadUnmanagedMultiSegment<T>(ref SequenceReader<byte> reader, out T value) where T : unmanaged
+        {
+            Debug.Assert(reader.UnreadSpan.Length < sizeof(T));
+
+            // Not enough data in the current segment, try to peek for the data we need.
+            T buffer = default;
+
+            Span<byte> tempSpan = new Span<byte>(&buffer, sizeof(T));
+
+            if (!reader.TryCopyTo(tempSpan))
+            {
+                value = default;
+                return false;
+            }
+
+            value = Unsafe.ReadUnaligned<T>(ref MemoryMarshal.GetReference(tempSpan));
+
+            reader.Advance(sizeof(T));
+
+            return true;
+        }
+    }
+}

+ 5 - 3
src/Ryujinx.Cpu/Jit/MemoryManagerHostTracked.cs

@@ -1,10 +1,12 @@
 using ARMeilleure.Memory;
+using Ryujinx.Common.Memory;
 using Ryujinx.Cpu.Jit.HostTracked;
 using Ryujinx.Cpu.Signal;
 using Ryujinx.Memory;
 using Ryujinx.Memory.Range;
 using Ryujinx.Memory.Tracking;
 using System;
+using System.Buffers;
 using System.Collections.Generic;
 using System.Linq;
 using System.Runtime.CompilerServices;
@@ -237,11 +239,11 @@ namespace Ryujinx.Cpu.Jit
             }
             else
             {
-                Memory<byte> memory = new byte[size];
+                IMemoryOwner<byte> memoryOwner = ByteMemoryPool.Rent(size);
 
-                Read(va, memory.Span);
+                Read(va, memoryOwner.Memory.Span);
 
-                return new WritableRegion(this, va, memory);
+                return new WritableRegion(this, va, memoryOwner);
             }
         }
 

+ 31 - 3
src/Ryujinx.Horizon.Generators/Hipc/HipcGenerator.cs

@@ -17,6 +17,8 @@ namespace Ryujinx.Horizon.Generators.Hipc
         private const string ResponseVariableName = "response";
         private const string OutRawDataVariableName = "outRawData";
 
+        private const string TypeSystemBuffersReadOnlySequence = "System.Buffers.ReadOnlySequence";
+        private const string TypeSystemMemory = "System.Memory";
         private const string TypeSystemReadOnlySpan = "System.ReadOnlySpan";
         private const string TypeSystemSpan = "System.Span";
         private const string TypeStructLayoutAttribute = "System.Runtime.InteropServices.StructLayoutAttribute";
@@ -329,7 +331,15 @@ namespace Ryujinx.Horizon.Generators.Hipc
                             value = $"{InObjectsVariableName}[{inObjectIndex++}]";
                             break;
                         case CommandArgType.Buffer:
-                            if (IsReadOnlySpan(compilation, parameter))
+                            if (IsMemory(compilation, parameter))
+                            {
+                                value = $"CommandSerialization.GetWritableRegion(processor.GetBufferRange({index}))";
+                            }
+                            else if (IsReadOnlySequence(compilation, parameter))
+                            {
+                                value = $"CommandSerialization.GetReadOnlySequence(processor.GetBufferRange({index}))";
+                            }
+                            else if (IsReadOnlySpan(compilation, parameter))
                             {
                                 string spanGenericTypeName = GetCanonicalTypeNameOfGenericArgument(compilation, parameter.Type, 0);
                                 value = GenerateSpanCast(spanGenericTypeName, $"CommandSerialization.GetReadOnlySpan(processor.GetBufferRange({index}))");
@@ -346,7 +356,13 @@ namespace Ryujinx.Horizon.Generators.Hipc
                             break;
                     }
 
-                    if (IsSpan(compilation, parameter))
+                    if (IsMemory(compilation, parameter))
+                    {
+                        generator.AppendLine($"using var {argName} = {value};");
+
+                        argName = $"{argName}.Memory";
+                    }
+                    else if (IsSpan(compilation, parameter))
                     {
                         generator.AppendLine($"using var {argName} = {value};");
 
@@ -637,7 +653,9 @@ namespace Ryujinx.Horizon.Generators.Hipc
 
         private static bool IsValidTypeForBuffer(Compilation compilation, ParameterSyntax parameter)
         {
-            return IsReadOnlySpan(compilation, parameter) ||
+            return IsMemory(compilation, parameter) ||
+                   IsReadOnlySequence(compilation, parameter) ||
+                   IsReadOnlySpan(compilation, parameter) ||
                    IsSpan(compilation, parameter) ||
                    IsUnmanagedType(compilation, parameter.Type);
         }
@@ -649,6 +667,16 @@ namespace Ryujinx.Horizon.Generators.Hipc
             return typeInfo.Type.IsUnmanagedType;
         }
 
+        private static bool IsMemory(Compilation compilation, ParameterSyntax parameter)
+        {
+            return GetCanonicalTypeName(compilation, parameter.Type) == TypeSystemMemory;
+        }
+
+        private static bool IsReadOnlySequence(Compilation compilation, ParameterSyntax parameter)
+        {
+            return GetCanonicalTypeName(compilation, parameter.Type) == TypeSystemBuffersReadOnlySequence;
+        }
+
         private static bool IsReadOnlySpan(Compilation compilation, ParameterSyntax parameter)
         {
             return GetCanonicalTypeName(compilation, parameter.Type) == TypeSystemReadOnlySpan;

+ 7 - 19
src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioRenderer.cs

@@ -57,23 +57,11 @@ namespace Ryujinx.Horizon.Sdk.Audio.Detail
 
         [CmifCommand(4)]
         public Result RequestUpdate(
-            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<byte> output,
-            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<byte> performanceOutput,
-            [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> input)
+            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Memory<byte> output,
+            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Memory<byte> performanceOutput,
+            [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySequence<byte> input)
         {
-            using IMemoryOwner<byte> outputOwner = ByteMemoryPool.Rent(output.Length);
-            using IMemoryOwner<byte> performanceOutputOwner = ByteMemoryPool.Rent(performanceOutput.Length);
-
-            Memory<byte> outputMemory = outputOwner.Memory;
-            Memory<byte> performanceOutputMemory = performanceOutputOwner.Memory;
-
-            using MemoryHandle outputHandle = outputMemory.Pin();
-            using MemoryHandle performanceOutputHandle = performanceOutputMemory.Pin();
-
-            Result result = new Result((int)_renderSystem.Update(outputMemory, performanceOutputMemory, input.ToArray()));
-
-            outputMemory.Span.CopyTo(output);
-            performanceOutputMemory.Span.CopyTo(performanceOutput);
+            Result result = new Result((int)_renderSystem.Update(output, performanceOutput, input));
 
             return result;
         }
@@ -127,9 +115,9 @@ namespace Ryujinx.Horizon.Sdk.Audio.Detail
 
         [CmifCommand(10)] // 3.0.0+
         public Result RequestUpdateAuto(
-            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.AutoSelect)] Span<byte> output,
-            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.AutoSelect)] Span<byte> performanceOutput,
-            [Buffer(HipcBufferFlags.In | HipcBufferFlags.AutoSelect)] ReadOnlySpan<byte> input)
+            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.AutoSelect)] Memory<byte> output,
+            [Buffer(HipcBufferFlags.Out | HipcBufferFlags.AutoSelect)] Memory<byte> performanceOutput,
+            [Buffer(HipcBufferFlags.In | HipcBufferFlags.AutoSelect)] ReadOnlySequence<byte> input)
         {
             return RequestUpdate(output, performanceOutput, input);
         }

+ 3 - 2
src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioRenderer.cs

@@ -1,6 +1,7 @@
 using Ryujinx.Horizon.Common;
 using Ryujinx.Horizon.Sdk.Sf;
 using System;
+using System.Buffers;
 
 namespace Ryujinx.Horizon.Sdk.Audio.Detail
 {
@@ -10,13 +11,13 @@ namespace Ryujinx.Horizon.Sdk.Audio.Detail
         Result GetSampleCount(out int sampleCount);
         Result GetMixBufferCount(out int mixBufferCount);
         Result GetState(out int state);
-        Result RequestUpdate(Span<byte> output, Span<byte> performanceOutput, ReadOnlySpan<byte> input);
+        Result RequestUpdate(Memory<byte> output, Memory<byte> performanceOutput, ReadOnlySequence<byte> input);
         Result Start();
         Result Stop();
         Result QuerySystemEvent(out int eventHandle);
         Result SetRenderingTimeLimit(int percent);
         Result GetRenderingTimeLimit(out int percent);
-        Result RequestUpdateAuto(Span<byte> output, Span<byte> performanceOutput, ReadOnlySpan<byte> input);
+        Result RequestUpdateAuto(Memory<byte> output, Memory<byte> performanceOutput, ReadOnlySequence<byte> input);
         Result ExecuteAudioRendererRendering();
         Result SetVoiceDropParameter(float voiceDropParameter);
         Result GetVoiceDropParameter(out float voiceDropParameter);

+ 6 - 0
src/Ryujinx.Horizon/Sdk/Sf/CommandSerialization.cs

@@ -2,6 +2,7 @@ using Ryujinx.Horizon.Sdk.Sf.Cmif;
 using Ryujinx.Horizon.Sdk.Sf.Hipc;
 using Ryujinx.Memory;
 using System;
+using System.Buffers;
 using System.Runtime.CompilerServices;
 using System.Runtime.InteropServices;
 
@@ -9,6 +10,11 @@ namespace Ryujinx.Horizon.Sdk.Sf
 {
     static class CommandSerialization
     {
+        public static ReadOnlySequence<byte> GetReadOnlySequence(PointerAndSize bufferRange)
+        {
+            return HorizonStatic.AddressSpace.GetReadOnlySequence(bufferRange.Address, checked((int)bufferRange.Size));
+        }
+
         public static ReadOnlySpan<byte> GetReadOnlySpan(PointerAndSize bufferRange)
         {
             return HorizonStatic.AddressSpace.GetSpan(bufferRange.Address, checked((int)bufferRange.Size));

+ 359 - 0
src/Ryujinx.Tests/Common/Extensions/SequenceReaderExtensionsTests.cs

@@ -0,0 +1,359 @@
+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));
+        }
+    }
+}