From c634eb4054c2e7f530307198d7cc6a20b3666d7d Mon Sep 17 00:00:00 2001
From: Logan Stromberg <loganstromberg@gmail.com>
Date: Mon, 20 May 2024 14:38:38 -0700
Subject: [PATCH] Updating Concentus dependency to speed up Opus decoding
 (#6757)

* Implementing new features in the latest Concentus library - span-in, span-out Opus decoding (so we don't have to make temporary buffer copies), returning a more precise error code from the decoder, and automatically linking the native opus library with P/invoke if supported on the current system

* Remove stub log messages and commit package upgrade to 2.1.0

* use more correct disposal pattern

* Bump to Concentus 2.1.1

* Bump to Concentus 2.1.2

* Don't bother pulling in native opus binaries from Concentus package (using ExcludeAssets).

* Fix opus MS channel count. Explicitly disable native lib probe in OpusCodecFactory.

* Bump to package 2.2.0 which has split out the native libs, as suggested.

---------

Co-authored-by: Logan Stromberg <lostromb@microsoft.com>
---
 Directory.Packages.props                      |  4 +-
 src/Ryujinx.Horizon/Ryujinx.Horizon.csproj    |  8 +-
 .../Sdk/Codec/Detail/HardwareOpusDecoder.cs   | 91 +++++++++++++------
 3 files changed, 68 insertions(+), 35 deletions(-)

diff --git a/Directory.Packages.props b/Directory.Packages.props
index d04e237e..739e66bd 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -11,7 +11,7 @@
     <PackageVersion Include="Avalonia.Svg" Version="11.0.0.18" />
     <PackageVersion Include="Avalonia.Svg.Skia" Version="11.0.0.18" />
     <PackageVersion Include="CommandLineParser" Version="2.9.1" />
-    <PackageVersion Include="Concentus" Version="1.1.7" />
+    <PackageVersion Include="Concentus" Version="2.2.0" />
     <PackageVersion Include="DiscordRichPresence" Version="1.2.1.24" />
     <PackageVersion Include="DynamicData" Version="8.4.1" />
     <PackageVersion Include="FluentAvaloniaUI" Version="2.0.5" />
@@ -49,4 +49,4 @@
     <PackageVersion Include="System.Management" Version="8.0.0" />
     <PackageVersion Include="UnicornEngine.Unicorn" Version="2.0.2-rc1-fb78016" />
   </ItemGroup>
-</Project>
+</Project>
\ No newline at end of file
diff --git a/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj b/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj
index d1f572d5..bf34ddd1 100644
--- a/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj
+++ b/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
     <TargetFramework>net8.0</TargetFramework>
@@ -16,10 +16,4 @@
     <PackageReference Include="Concentus" />
     <PackageReference Include="LibHac" />
   </ItemGroup>
-
-  <!-- Due to Concentus. -->
-  <PropertyGroup>
-    <NoWarn>NU1605</NoWarn>
-  </PropertyGroup>
-
 </Project>
diff --git a/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoder.cs b/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoder.cs
index 5d279858..2146362d 100644
--- a/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoder.cs
+++ b/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoder.cs
@@ -14,6 +14,11 @@ namespace Ryujinx.Horizon.Sdk.Codec.Detail
 {
     partial class HardwareOpusDecoder : IHardwareOpusDecoder, IDisposable
     {
+        static HardwareOpusDecoder()
+        {
+            OpusCodecFactory.AttemptToUseNativeLibrary = false;
+        }
+
         [StructLayout(LayoutKind.Sequential)]
         private struct OpusPacketHeader
         {
@@ -30,60 +35,87 @@ namespace Ryujinx.Horizon.Sdk.Codec.Detail
             }
         }
 
-        private interface IDecoder
+        private interface IDecoder : IDisposable
         {
             int SampleRate { get; }
             int ChannelsCount { get; }
 
-            int Decode(byte[] inData, int inDataOffset, int len, short[] outPcm, int outPcmOffset, int frameSize);
+            int Decode(ReadOnlySpan<byte> inData, Span<short> outPcm, int frameSize);
             void ResetState();
         }
 
         private class Decoder : IDecoder
         {
-            private readonly OpusDecoder _decoder;
+            private readonly IOpusDecoder _decoder;
 
             public int SampleRate => _decoder.SampleRate;
             public int ChannelsCount => _decoder.NumChannels;
 
             public Decoder(int sampleRate, int channelsCount)
             {
-                _decoder = new OpusDecoder(sampleRate, channelsCount);
+                _decoder = OpusCodecFactory.CreateDecoder(sampleRate, channelsCount);
             }
 
-            public int Decode(byte[] inData, int inDataOffset, int len, short[] outPcm, int outPcmOffset, int frameSize)
+            public int Decode(ReadOnlySpan<byte> inData, Span<short> outPcm, int frameSize)
             {
-                return _decoder.Decode(inData, inDataOffset, len, outPcm, outPcmOffset, frameSize);
+                return _decoder.Decode(inData, outPcm, frameSize);
             }
 
             public void ResetState()
             {
                 _decoder.ResetState();
             }
+
+            public void Dispose()
+            {
+                Dispose(disposing: true);
+                GC.SuppressFinalize(this);
+            }
+
+            protected virtual void Dispose(bool disposing)
+            {
+                if (disposing)
+                {
+                    _decoder?.Dispose();
+                }
+            }
         }
 
         private class MultiSampleDecoder : IDecoder
         {
-            private readonly OpusMSDecoder _decoder;
+            private readonly IOpusMultiStreamDecoder _decoder;
 
             public int SampleRate => _decoder.SampleRate;
-            public int ChannelsCount { get; }
+            public int ChannelsCount => _decoder.NumChannels;
 
             public MultiSampleDecoder(int sampleRate, int channelsCount, int streams, int coupledStreams, byte[] mapping)
             {
-                ChannelsCount = channelsCount;
-                _decoder = new OpusMSDecoder(sampleRate, channelsCount, streams, coupledStreams, mapping);
+                _decoder = OpusCodecFactory.CreateMultiStreamDecoder(sampleRate, channelsCount, streams, coupledStreams, mapping);
             }
 
-            public int Decode(byte[] inData, int inDataOffset, int len, short[] outPcm, int outPcmOffset, int frameSize)
+            public int Decode(ReadOnlySpan<byte> inData, Span<short> outPcm, int frameSize)
             {
-                return _decoder.DecodeMultistream(inData, inDataOffset, len, outPcm, outPcmOffset, frameSize, 0);
+                return _decoder.DecodeMultistream(inData, outPcm, frameSize, false);
             }
 
             public void ResetState()
             {
                 _decoder.ResetState();
             }
+
+            public void Dispose()
+            {
+                Dispose(disposing: true);
+                GC.SuppressFinalize(this);
+            }
+
+            protected virtual void Dispose(bool disposing)
+            {
+                if (disposing)
+                {
+                    _decoder?.Dispose();
+                }
+            }
         }
 
         private readonly IDecoder _decoder;
@@ -221,7 +253,8 @@ namespace Ryujinx.Horizon.Sdk.Codec.Detail
         {
             timeTaken = 0;
 
-            Result result = DecodeInterleaved(_decoder, reset, input, out short[] outPcmData, output.Length, out outConsumed, out outSamples);
+            Span<short> outPcmSpace = MemoryMarshal.Cast<byte, short>(output);
+            Result result = DecodeInterleaved(_decoder, reset, input, outPcmSpace, output.Length, out outConsumed, out outSamples);
 
             if (withPerf)
             {
@@ -229,14 +262,12 @@ namespace Ryujinx.Horizon.Sdk.Codec.Detail
                 timeTaken = 0;
             }
 
-            MemoryMarshal.Cast<short, byte>(outPcmData).CopyTo(output[..(outPcmData.Length * sizeof(short))]);
-
             return result;
         }
 
-        private static Result GetPacketNumSamples(IDecoder decoder, out int numSamples, byte[] packet)
+        private static Result GetPacketNumSamples(IDecoder decoder, out int numSamples, ReadOnlySpan<byte> packet)
         {
-            int result = OpusPacketInfo.GetNumSamples(packet, 0, packet.Length, decoder.SampleRate);
+            int result = OpusPacketInfo.GetNumSamples(packet, decoder.SampleRate);
 
             numSamples = result;
 
@@ -256,12 +287,11 @@ namespace Ryujinx.Horizon.Sdk.Codec.Detail
             IDecoder decoder,
             bool reset,
             ReadOnlySpan<byte> input,
-            out short[] outPcmData,
+            Span<short> outPcmData,
             int outputSize,
             out int outConsumed,
             out int outSamples)
         {
-            outPcmData = null;
             outConsumed = 0;
             outSamples = 0;
 
@@ -281,7 +311,7 @@ namespace Ryujinx.Horizon.Sdk.Codec.Detail
                 return CodecResult.InvalidLength;
             }
 
-            byte[] opusData = input.Slice(headerSize, (int)header.Length).ToArray();
+            ReadOnlySpan<byte> opusData = input.Slice(headerSize, (int)header.Length);
 
             Result result = GetPacketNumSamples(decoder, out int numSamples, opusData);
 
@@ -292,8 +322,6 @@ namespace Ryujinx.Horizon.Sdk.Codec.Detail
                     return CodecResult.InvalidLength;
                 }
 
-                outPcmData = new short[numSamples * decoder.ChannelsCount];
-
                 if (reset)
                 {
                     decoder.ResetState();
@@ -301,13 +329,22 @@ namespace Ryujinx.Horizon.Sdk.Codec.Detail
 
                 try
                 {
-                    outSamples = decoder.Decode(opusData, 0, opusData.Length, outPcmData, 0, outPcmData.Length / decoder.ChannelsCount);
+                    outSamples = decoder.Decode(opusData, outPcmData, numSamples);
                     outConsumed = (int)totalSize;
                 }
-                catch (OpusException)
+                catch (OpusException e)
                 {
-                    // TODO: As OpusException doesn't return the exact error code, this is inaccurate in some cases...
-                    return CodecResult.InvalidLength;
+                    switch (e.OpusErrorCode)
+                    {
+                        case OpusError.OPUS_BUFFER_TOO_SMALL:
+                            return CodecResult.InvalidLength;
+                        case OpusError.OPUS_BAD_ARG:
+                            return CodecResult.OpusBadArg;
+                        case OpusError.OPUS_INVALID_PACKET:
+                            return CodecResult.OpusInvalidPacket;
+                        default:
+                            return CodecResult.InvalidLength;
+                    }
                 }
             }
 
@@ -324,6 +361,8 @@ namespace Ryujinx.Horizon.Sdk.Codec.Detail
 
                     _workBufferHandle = 0;
                 }
+
+                _decoder?.Dispose();
             }
         }