using Ryujinx.Audio.Renderer.Common; using Ryujinx.Audio.Renderer.Dsp.State; 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; namespace Ryujinx.Audio.Renderer.Server.Splitter { /// /// Splitter context. /// public class SplitterContext { /// /// Amount of biquad filter states per splitter destination. /// public const int BqfStatesPerDestination = 4; /// /// Storage for . /// private Memory _splitters; /// /// Storage for . /// private Memory _splitterDestinationsV1; /// /// Storage for . /// private Memory _splitterDestinationsV2; /// /// Splitter biquad filtering states. /// private Memory _splitterBqfStates; /// /// Version of the splitter context that is being used, currently can be 1 or 2. /// public int Version { get; private set; } /// /// If set to true, trust the user destination count in . /// public bool IsBugFixed { get; private set; } /// /// Initialize . /// /// The behaviour context. /// The audio renderer configuration. /// The . /// Memory to store the biquad filtering state for splitters during processing. /// Return true if the initialization was successful. public bool Initialize( ref BehaviourContext behaviourContext, ref AudioRendererConfiguration parameter, WorkBufferAllocator workBufferAllocator, Memory splitterBqfStates) { if (!behaviourContext.IsSplitterSupported() || parameter.SplitterCount <= 0 || parameter.SplitterDestinationCount <= 0) { Setup(Memory.Empty, Memory.Empty, Memory.Empty, false); return true; } Memory splitters = workBufferAllocator.Allocate(parameter.SplitterCount, SplitterState.Alignment); if (splitters.IsEmpty) { return false; } int splitterId = 0; foreach (ref SplitterState splitter in splitters.Span) { splitter = new SplitterState(splitterId++); } Memory splitterDestinationsV1 = Memory.Empty; Memory splitterDestinationsV2 = Memory.Empty; if (!behaviourContext.IsBiquadFilterParameterForSplitterEnabled()) { Version = 1; splitterDestinationsV1 = workBufferAllocator.Allocate(parameter.SplitterDestinationCount, SplitterDestinationVersion1.Alignment); if (splitterDestinationsV1.IsEmpty) { return false; } int splitterDestinationId = 0; foreach (ref SplitterDestinationVersion1 data in splitterDestinationsV1.Span) { data = new SplitterDestinationVersion1(splitterDestinationId++); } } else { Version = 2; splitterDestinationsV2 = workBufferAllocator.Allocate(parameter.SplitterDestinationCount, SplitterDestinationVersion2.Alignment); if (splitterDestinationsV2.IsEmpty) { return false; } int splitterDestinationId = 0; foreach (ref SplitterDestinationVersion2 data in splitterDestinationsV2.Span) { data = new SplitterDestinationVersion2(splitterDestinationId++); } if (parameter.SplitterDestinationCount > 0) { // Official code stores it in the SplitterDestinationVersion2 struct, // but we don't to avoid using unsafe code. splitterBqfStates.Span.Clear(); _splitterBqfStates = splitterBqfStates; } else { _splitterBqfStates = Memory.Empty; } } SplitterState.InitializeSplitters(splitters.Span); Setup(splitters, splitterDestinationsV1, splitterDestinationsV2, behaviourContext.IsSplitterBugFixed()); return true; } /// /// Get the work buffer size while adding the size needed for splitter to operate. /// /// The current size. /// The behaviour context. /// The renderer configuration. /// Return the new size taking splitter into account. public static ulong GetWorkBufferSize(ulong size, ref BehaviourContext behaviourContext, ref AudioRendererConfiguration parameter) { if (behaviourContext.IsSplitterSupported()) { size = WorkBufferAllocator.GetTargetSize(size, parameter.SplitterCount, SplitterState.Alignment); if (behaviourContext.IsBiquadFilterParameterForSplitterEnabled()) { size = WorkBufferAllocator.GetTargetSize(size, parameter.SplitterDestinationCount, SplitterDestinationVersion2.Alignment); } else { size = WorkBufferAllocator.GetTargetSize(size, parameter.SplitterDestinationCount, SplitterDestinationVersion1.Alignment); } if (behaviourContext.IsSplitterBugFixed()) { size = WorkBufferAllocator.GetTargetSize(size, parameter.SplitterDestinationCount, 0x10); } return size; } return size; } /// /// Setup the instance. /// /// The storage. /// The storage. /// The storage. /// If set to true, trust the user destination count in . private void Setup( Memory splitters, Memory splitterDestinationsV1, Memory splitterDestinationsV2, bool isBugFixed) { _splitters = splitters; _splitterDestinationsV1 = splitterDestinationsV1; _splitterDestinationsV2 = splitterDestinationsV2; IsBugFixed = isBugFixed; } /// /// Clear the new connection flag. /// private void ClearAllNewConnectionFlag() { foreach (ref SplitterState splitter in _splitters.Span) { splitter.ClearNewConnectionFlag(); } } /// /// Get the destination count using the count of splitter. /// /// The destination count using the count of splitter. public int GetDestinationCountPerStateForCompatibility() { if (_splitters.IsEmpty) { return 0; } int length = _splitterDestinationsV2.IsEmpty ? _splitterDestinationsV1.Length : _splitterDestinationsV2.Length; return length / _splitters.Length; } /// /// Update one or multiple from user parameters. /// /// The splitter header. /// The raw data after the splitter header. private void UpdateState(in SplitterInParameterHeader inputHeader, ref SequenceReader input) { for (int i = 0; i < inputHeader.SplitterCount; i++) { ref readonly SplitterInParameter parameter = ref input.GetRefOrRefToCopy(out _); Debug.Assert(parameter.IsMagicValid()); if (parameter.IsMagicValid()) { if (parameter.Id >= 0 && parameter.Id < _splitters.Length) { ref SplitterState splitter = ref GetState(parameter.Id); splitter.Update(this, in parameter, ref input); } // NOTE: there are 12 bytes of unused/unknown data after the destination IDs array. input.Advance(0xC); } else { input.Rewind(Unsafe.SizeOf()); break; } } } /// /// Update one splitter destination data from user parameters. /// /// The raw data after the splitter header. /// True if the update was successful, false otherwise private bool UpdateData(ref SequenceReader input) where T : unmanaged, ISplitterDestinationInParameter { ref readonly T parameter = ref input.GetRefOrRefToCopy(out _); Debug.Assert(parameter.IsMagicValid()); if (parameter.IsMagicValid()) { int length = _splitterDestinationsV2.IsEmpty ? _splitterDestinationsV1.Length : _splitterDestinationsV2.Length; if (parameter.Id >= 0 && parameter.Id < length) { SplitterDestination destination = GetDestination(parameter.Id); destination.Update(parameter); } return true; } else { input.Rewind(Unsafe.SizeOf()); return false; } } /// /// Update one or multiple splitter destination data from user parameters. /// /// The splitter header. /// The raw data after the splitter header. private void UpdateData(in SplitterInParameterHeader inputHeader, ref SequenceReader input) { for (int i = 0; i < inputHeader.SplitterDestinationCount; i++) { if (Version == 1) { if (!UpdateData(ref input)) { break; } } else if (Version == 2) { if (!UpdateData(ref input)) { break; } } else { Debug.Fail($"Invalid splitter context version {Version}."); } } } /// /// Update splitter from user parameters. /// /// The input raw user data. /// Return true if the update was successful. public bool Update(ref SequenceReader input) { if (!UsingSplitter()) { return true; } ref readonly SplitterInParameterHeader header = ref input.GetRefOrRefToCopy(out _); if (header.IsMagicValid()) { ClearAllNewConnectionFlag(); UpdateState(in header, ref input); UpdateData(in header, ref input); input.SetConsumed(BitUtils.AlignUp(input.Consumed, 0x10)); return true; } else { input.Rewind(Unsafe.SizeOf()); return false; } } /// /// Get a reference to a at the given . /// /// The index to use. /// A reference to a at the given . public ref SplitterState GetState(int id) { return ref SpanIOHelper.GetFromMemory(_splitters, id, (uint)_splitters.Length); } /// /// Get a reference to the splitter destination data at the given . /// /// The index to use. /// A reference to the splitter destination data at the given . public SplitterDestination GetDestination(int id) { if (_splitterDestinationsV2.IsEmpty) { return new SplitterDestination(ref SpanIOHelper.GetFromMemory(_splitterDestinationsV1, id, (uint)_splitterDestinationsV1.Length)); } else { return new SplitterDestination(ref SpanIOHelper.GetFromMemory(_splitterDestinationsV2, id, (uint)_splitterDestinationsV2.Length)); } } /// /// Get a in the at and pass to . /// /// The index to use to get the . /// The index of the . /// A . public SplitterDestination GetDestination(int id, int destinationId) { ref SplitterState splitter = ref GetState(id); return splitter.GetData(destinationId); } /// /// Gets the biquad filter state for a given splitter destination. /// /// The splitter destination. /// Biquad filter state for the specified destination. public Memory GetBiquadFilterState(SplitterDestination destination) { return _splitterBqfStates.Slice(destination.Id * BqfStatesPerDestination, BqfStatesPerDestination); } /// /// Return true if the audio renderer has any splitters. /// /// True if the audio renderer has any splitters. public bool UsingSplitter() { return !_splitters.IsEmpty && (!_splitterDestinationsV1.IsEmpty || !_splitterDestinationsV2.IsEmpty); } /// /// Update the internal state of all splitters. /// public void UpdateInternalState() { foreach (ref SplitterState splitter in _splitters.Span) { splitter.UpdateInternalState(); } } } }