diff options
Diffstat (limited to 'Ryujinx.Audio/Renderer/Server')
53 files changed, 11124 insertions, 0 deletions
diff --git a/Ryujinx.Audio/Renderer/Server/AudioRenderSystem.cs b/Ryujinx.Audio/Renderer/Server/AudioRenderSystem.cs new file mode 100644 index 00000000..787b8f9f --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/AudioRenderSystem.cs @@ -0,0 +1,838 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Integration; +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Dsp.Command; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Server.Effect; +using Ryujinx.Audio.Renderer.Server.MemoryPool; +using Ryujinx.Audio.Renderer.Server.Mix; +using Ryujinx.Audio.Renderer.Server.Performance; +using Ryujinx.Audio.Renderer.Server.Sink; +using Ryujinx.Audio.Renderer.Server.Splitter; +using Ryujinx.Audio.Renderer.Server.Types; +using Ryujinx.Audio.Renderer.Server.Upsampler; +using Ryujinx.Audio.Renderer.Server.Voice; +using Ryujinx.Audio.Renderer.Utils; +using Ryujinx.Common; +using Ryujinx.Common.Logging; +using Ryujinx.Memory; +using System; +using System.Buffers; +using System.Diagnostics; +using System.Threading; + +using CpuAddress = System.UInt64; + +namespace Ryujinx.Audio.Renderer.Server +{ + public class AudioRenderSystem : IDisposable + { + private object _lock = new object(); + + private AudioRendererExecutionMode _executionMode; + private IWritableEvent _systemEvent; + private ManualResetEvent _terminationEvent; + private MemoryPoolState _dspMemoryPoolState; + private VoiceContext _voiceContext; + private MixContext _mixContext; + private SinkContext _sinkContext; + private SplitterContext _splitterContext; + private EffectContext _effectContext; + private PerformanceManager _performanceManager; + private UpsamplerManager _upsamplerManager; + private bool _isActive; + private BehaviourContext _behaviourContext; + private ulong _totalElapsedTicksUpdating; + private ulong _totalElapsedTicks; + private int _sessionId; + private Memory<MemoryPoolState> _memoryPools; + + private uint _sampleRate; + private uint _sampleCount; + private uint _mixBufferCount; + private uint _voiceChannelCountMax; + private uint _upsamplerCount; + private uint _memoryPoolCount; + private uint _processHandle; + private ulong _appletResourceId; + + private WritableRegion _workBufferRegion; + private MemoryHandle _workBufferMemoryPin; + + private Memory<float> _mixBuffer; + private Memory<float> _depopBuffer; + + private uint _renderingTimeLimitPercent; + private bool _voiceDropEnabled; + private uint _voiceDropCount; + private bool _isDspRunningBehind; + + private ICommandProcessingTimeEstimator _commandProcessingTimeEstimator; + + private Memory<byte> _performanceBuffer; + + public IVirtualMemoryManager MemoryManager { get; private set; } + + private ulong _elapsedFrameCount; + private ulong _renderingStartTick; + + private AudioRendererManager _manager; + + public AudioRenderSystem(AudioRendererManager manager, IWritableEvent systemEvent) + { + _manager = manager; + _terminationEvent = new ManualResetEvent(false); + _dspMemoryPoolState = MemoryPoolState.Create(MemoryPoolState.LocationType.Dsp); + _voiceContext = new VoiceContext(); + _mixContext = new MixContext(); + _sinkContext = new SinkContext(); + _splitterContext = new SplitterContext(); + _effectContext = new EffectContext(); + + _commandProcessingTimeEstimator = null; + _systemEvent = systemEvent; + _behaviourContext = new BehaviourContext(); + + _totalElapsedTicksUpdating = 0; + _sessionId = 0; + } + + public ResultCode Initialize(ref AudioRendererConfiguration parameter, uint processHandle, CpuAddress workBuffer, ulong workBufferSize, int sessionId, ulong appletResourceId, IVirtualMemoryManager memoryManager) + { + if (!BehaviourContext.CheckValidRevision(parameter.Revision)) + { + return ResultCode.OperationFailed; + } + + if (GetWorkBufferSize(ref parameter) > workBufferSize) + { + return ResultCode.WorkBufferTooSmall; + } + + Debug.Assert(parameter.RenderingDevice == AudioRendererRenderingDevice.Dsp && parameter.ExecutionMode == AudioRendererExecutionMode.Auto); + + Logger.Info?.Print(LogClass.AudioRenderer, $"Initializing with REV{BehaviourContext.GetRevisionNumber(parameter.Revision)}"); + + _behaviourContext.SetUserRevision(parameter.Revision); + + _sampleRate = parameter.SampleRate; + _sampleCount = parameter.SampleCount; + _mixBufferCount = parameter.MixBufferCount; + _voiceChannelCountMax = Constants.VoiceChannelCountMax; + _upsamplerCount = parameter.SinkCount + parameter.SubMixBufferCount; + _appletResourceId = appletResourceId; + _memoryPoolCount = parameter.EffectCount + parameter.VoiceCount * Constants.VoiceWaveBufferCount; + _executionMode = parameter.ExecutionMode; + _sessionId = sessionId; + MemoryManager = memoryManager; + + WorkBufferAllocator workBufferAllocator; + + _workBufferRegion = MemoryManager.GetWritableRegion(workBuffer, (int)workBufferSize); + _workBufferRegion.Memory.Span.Fill(0); + _workBufferMemoryPin = _workBufferRegion.Memory.Pin(); + + workBufferAllocator = new WorkBufferAllocator(_workBufferRegion.Memory); + + PoolMapper poolMapper = new PoolMapper(processHandle, false); + poolMapper.InitializeSystemPool(ref _dspMemoryPoolState, workBuffer, workBufferSize); + + _mixBuffer = workBufferAllocator.Allocate<float>(_sampleCount * (_voiceChannelCountMax + _mixBufferCount), 0x10); + + if (_mixBuffer.IsEmpty) + { + return ResultCode.WorkBufferTooSmall; + } + + Memory<float> upSamplerWorkBuffer = workBufferAllocator.Allocate<float>(Constants.TargetSampleCount * (_voiceChannelCountMax + _mixBufferCount) * _upsamplerCount, 0x10); + + if (upSamplerWorkBuffer.IsEmpty) + { + return ResultCode.WorkBufferTooSmall; + } + + _depopBuffer = workBufferAllocator.Allocate<float>((ulong)BitUtils.AlignUp(parameter.MixBufferCount, Constants.BufferAlignment), Constants.BufferAlignment); + + if (_depopBuffer.IsEmpty) + { + return ResultCode.WorkBufferTooSmall; + } + + // Invalidate DSP cache on what was currently allocated with workBuffer. + AudioProcessorMemoryManager.InvalidateDspCache(_dspMemoryPoolState.Translate(workBuffer, workBufferAllocator.Offset), workBufferAllocator.Offset); + + Debug.Assert((workBufferAllocator.Offset % Constants.BufferAlignment) == 0); + + Memory<VoiceState> voices = workBufferAllocator.Allocate<VoiceState>(parameter.VoiceCount, VoiceState.Alignment); + + if (voices.IsEmpty) + { + return ResultCode.WorkBufferTooSmall; + } + + foreach (ref VoiceState voice in voices.Span) + { + voice.Initialize(); + } + + // A pain to handle as we can't have VoiceState*, use indices to be a bit more safe + Memory<int> sortedVoices = workBufferAllocator.Allocate<int>(parameter.VoiceCount, 0x10); + + if (sortedVoices.IsEmpty) + { + return ResultCode.WorkBufferTooSmall; + } + + // Clear memory (use -1 as it's an invalid index) + sortedVoices.Span.Fill(-1); + + Memory<VoiceChannelResource> voiceChannelResources = workBufferAllocator.Allocate<VoiceChannelResource>(parameter.VoiceCount, VoiceChannelResource.Alignment); + + if (voiceChannelResources.IsEmpty) + { + return ResultCode.WorkBufferTooSmall; + } + + for (uint id = 0; id < voiceChannelResources.Length; id++) + { + ref VoiceChannelResource voiceChannelResource = ref voiceChannelResources.Span[(int)id]; + + voiceChannelResource.Id = id; + voiceChannelResource.IsUsed = false; + } + + Memory<VoiceUpdateState> voiceUpdateStates = workBufferAllocator.Allocate<VoiceUpdateState>(parameter.VoiceCount, VoiceUpdateState.Align); + + if (voiceUpdateStates.IsEmpty) + { + return ResultCode.WorkBufferTooSmall; + } + + uint mixesCount = parameter.SubMixBufferCount + 1; + + Memory<MixState> mixes = workBufferAllocator.Allocate<MixState>(mixesCount, MixState.Alignment); + + if (mixes.IsEmpty) + { + return ResultCode.WorkBufferTooSmall; + } + + if (parameter.EffectCount == 0) + { + foreach (ref MixState mix in mixes.Span) + { + mix = new MixState(Memory<int>.Empty, ref _behaviourContext); + } + } + else + { + Memory<int> effectProcessingOrderArray = workBufferAllocator.Allocate<int>(parameter.EffectCount * mixesCount, 0x10); + + foreach (ref MixState mix in mixes.Span) + { + mix = new MixState(effectProcessingOrderArray.Slice(0, (int)parameter.EffectCount), ref _behaviourContext); + + effectProcessingOrderArray = effectProcessingOrderArray.Slice((int)parameter.EffectCount); + } + } + + // Initialize the final mix id + mixes.Span[0].MixId = Constants.FinalMixId; + + Memory<int> sortedMixesState = workBufferAllocator.Allocate<int>(mixesCount, 0x10); + + if (sortedMixesState.IsEmpty) + { + return ResultCode.WorkBufferTooSmall; + } + + // Clear memory (use -1 as it's an invalid index) + sortedMixesState.Span.Fill(-1); + + Memory<byte> nodeStatesWorkBuffer = Memory<byte>.Empty; + Memory<byte> edgeMatrixWorkBuffer = Memory<byte>.Empty; + + if (_behaviourContext.IsSplitterSupported()) + { + nodeStatesWorkBuffer = workBufferAllocator.Allocate((uint)NodeStates.GetWorkBufferSize((int)mixesCount), 1); + edgeMatrixWorkBuffer = workBufferAllocator.Allocate((uint)EdgeMatrix.GetWorkBufferSize((int)mixesCount), 1); + + if (nodeStatesWorkBuffer.IsEmpty || edgeMatrixWorkBuffer.IsEmpty) + { + return ResultCode.WorkBufferTooSmall; + } + } + + _mixContext.Initialize(sortedMixesState, mixes, nodeStatesWorkBuffer, edgeMatrixWorkBuffer); + + _memoryPools = workBufferAllocator.Allocate<MemoryPoolState>(_memoryPoolCount, MemoryPoolState.Alignment); + + if (_memoryPools.IsEmpty) + { + return ResultCode.WorkBufferTooSmall; + } + + foreach (ref MemoryPoolState state in _memoryPools.Span) + { + state = MemoryPoolState.Create(MemoryPoolState.LocationType.Cpu); + } + + if (!_splitterContext.Initialize(ref _behaviourContext, ref parameter, workBufferAllocator)) + { + return ResultCode.WorkBufferTooSmall; + } + + _processHandle = processHandle; + + _upsamplerManager = new UpsamplerManager(upSamplerWorkBuffer, _upsamplerCount); + + _effectContext.Initialize(parameter.EffectCount); + _sinkContext.Initialize(parameter.SinkCount); + + Memory<VoiceUpdateState> voiceUpdateStatesDsp = workBufferAllocator.Allocate<VoiceUpdateState>(parameter.VoiceCount, VoiceUpdateState.Align); + + if (voiceUpdateStatesDsp.IsEmpty) + { + return ResultCode.WorkBufferTooSmall; + } + + _voiceContext.Initialize(sortedVoices, voices, voiceChannelResources, voiceUpdateStates, voiceUpdateStatesDsp, parameter.VoiceCount); + + if (parameter.PerformanceMetricFramesCount > 0) + { + ulong performanceBufferSize = PerformanceManager.GetRequiredBufferSizeForPerformanceMetricsPerFrame(ref parameter, ref _behaviourContext) * (parameter.PerformanceMetricFramesCount + 1) + 0xC; + + _performanceBuffer = workBufferAllocator.Allocate(performanceBufferSize, Constants.BufferAlignment); + + if (_performanceBuffer.IsEmpty) + { + return ResultCode.WorkBufferTooSmall; + } + + _performanceManager = PerformanceManager.Create(_performanceBuffer, ref parameter, _behaviourContext); + } + else + { + _performanceManager = null; + } + + _totalElapsedTicksUpdating = 0; + _totalElapsedTicks = 0; + _renderingTimeLimitPercent = 100; + _voiceDropEnabled = parameter.VoiceDropEnabled && _executionMode == AudioRendererExecutionMode.Auto; + + AudioProcessorMemoryManager.InvalidateDataCache(workBuffer, workBufferSize); + + _processHandle = processHandle; + _elapsedFrameCount = 0; + + switch (_behaviourContext.GetCommandProcessingTimeEstimatorVersion()) + { + case 1: + _commandProcessingTimeEstimator = new CommandProcessingTimeEstimatorVersion1(_sampleCount, _mixBufferCount); + break; + case 2: + _commandProcessingTimeEstimator = new CommandProcessingTimeEstimatorVersion2(_sampleCount, _mixBufferCount); + break; + case 3: + _commandProcessingTimeEstimator = new CommandProcessingTimeEstimatorVersion3(_sampleCount, _mixBufferCount); + break; + default: + throw new NotImplementedException($"Unsupported processing time estimator version {_behaviourContext.GetCommandProcessingTimeEstimatorVersion()}."); + } + + return ResultCode.Success; + } + + public void Start() + { + Logger.Info?.Print(LogClass.AudioRenderer, $"Starting renderer id {_sessionId}"); + + lock (_lock) + { + _elapsedFrameCount = 0; + _isActive = true; + } + } + + public void Stop() + { + Logger.Info?.Print(LogClass.AudioRenderer, $"Stopping renderer id {_sessionId}"); + + lock (_lock) + { + _isActive = false; + } + + if (_executionMode == AudioRendererExecutionMode.Auto) + { + _terminationEvent.WaitOne(); + } + + Logger.Info?.Print(LogClass.AudioRenderer, $"Stopped renderer id {_sessionId}"); + } + + public ResultCode Update(Memory<byte> output, Memory<byte> performanceOutput, ReadOnlyMemory<byte> input) + { + lock (_lock) + { + ulong updateStartTicks = GetSystemTicks(); + + output.Span.Fill(0); + + StateUpdater stateUpdater = new StateUpdater(input, output, _processHandle, _behaviourContext); + + ResultCode result; + + result = stateUpdater.UpdateBehaviourContext(); + + if (result != ResultCode.Success) + { + return result; + } + + result = stateUpdater.UpdateMemoryPools(_memoryPools.Span); + + if (result != ResultCode.Success) + { + return result; + } + + result = stateUpdater.UpdateVoiceChannelResources(_voiceContext); + + if (result != ResultCode.Success) + { + return result; + } + + result = stateUpdater.UpdateVoices(_voiceContext, _memoryPools); + + if (result != ResultCode.Success) + { + return result; + } + + result = stateUpdater.UpdateEffects(_effectContext, _isActive, _memoryPools); + + if (result != ResultCode.Success) + { + return result; + } + + if (_behaviourContext.IsSplitterSupported()) + { + result = stateUpdater.UpdateSplitter(_splitterContext); + + if (result != ResultCode.Success) + { + return result; + } + } + + result = stateUpdater.UpdateMixes(_mixContext, GetMixBufferCount(), _effectContext, _splitterContext); + + if (result != ResultCode.Success) + { + return result; + } + + result = stateUpdater.UpdateSinks(_sinkContext, _memoryPools); + + if (result != ResultCode.Success) + { + return result; + } + + result = stateUpdater.UpdatePerformanceBuffer(_performanceManager, performanceOutput.Span); + + if (result != ResultCode.Success) + { + return result; + } + + result = stateUpdater.UpdateErrorInfo(); + + if (result != ResultCode.Success) + { + return result; + } + + if (_behaviourContext.IsElapsedFrameCountSupported()) + { + result = stateUpdater.UpdateRendererInfo(_elapsedFrameCount); + + if (result != ResultCode.Success) + { + return result; + } + } + + result = stateUpdater.CheckConsumedSize(); + + if (result != ResultCode.Success) + { + return result; + } + + _systemEvent.Clear(); + + ulong updateEndTicks = GetSystemTicks(); + + _totalElapsedTicksUpdating += (updateEndTicks - updateStartTicks); + + return result; + } + } + + private ulong GetSystemTicks() + { + double ticks = ARMeilleure.State.ExecutionContext.ElapsedTicks * ARMeilleure.State.ExecutionContext.TickFrequency; + + return (ulong)(ticks * Constants.TargetTimerFrequency); + } + + private uint ComputeVoiceDrop(CommandBuffer commandBuffer, long voicesEstimatedTime, long deltaTimeDsp) + { + int i; + + for (i = 0; i < commandBuffer.CommandList.Commands.Count; i++) + { + ICommand command = commandBuffer.CommandList.Commands[i]; + + CommandType commandType = command.CommandType; + + if (commandType == CommandType.AdpcmDataSourceVersion1 || + commandType == CommandType.AdpcmDataSourceVersion2 || + commandType == CommandType.PcmInt16DataSourceVersion1 || + commandType == CommandType.PcmInt16DataSourceVersion2 || + commandType == CommandType.PcmFloatDataSourceVersion1 || + commandType == CommandType.PcmFloatDataSourceVersion2 || + commandType == CommandType.Performance) + { + break; + } + } + + uint voiceDropped = 0; + + for (; i < commandBuffer.CommandList.Commands.Count; i++) + { + ICommand targetCommand = commandBuffer.CommandList.Commands[i]; + + int targetNodeId = targetCommand.NodeId; + + if (voicesEstimatedTime <= deltaTimeDsp || NodeIdHelper.GetType(targetNodeId) != NodeIdType.Voice) + { + break; + } + + ref VoiceState voice = ref _voiceContext.GetState(NodeIdHelper.GetBase(targetNodeId)); + + if (voice.Priority == Constants.VoiceHighestPriority) + { + break; + } + + // We can safely drop this voice, disable all associated commands while activating depop preparation commands. + voiceDropped++; + voice.VoiceDropFlag = true; + + Logger.Warning?.Print(LogClass.AudioRenderer, $"Dropping voice {voice.NodeId}"); + + for (; i < commandBuffer.CommandList.Commands.Count; i++) + { + ICommand command = commandBuffer.CommandList.Commands[i]; + + if (command.NodeId != targetNodeId) + { + break; + } + + if (command.CommandType == CommandType.DepopPrepare) + { + command.Enabled = true; + } + else if (command.CommandType == CommandType.Performance || !command.Enabled) + { + continue; + } + else + { + command.Enabled = false; + + voicesEstimatedTime -= (long)command.EstimatedProcessingTime; + } + } + } + + return voiceDropped; + } + + private void GenerateCommandList(out CommandList commandList) + { + Debug.Assert(_executionMode == AudioRendererExecutionMode.Auto); + + PoolMapper.ClearUsageState(_memoryPools); + + ulong startTicks = GetSystemTicks(); + + commandList = new CommandList(this); + + if (_performanceManager != null) + { + _performanceManager.TapFrame(_isDspRunningBehind, _voiceDropCount, _renderingStartTick); + + _isDspRunningBehind = false; + _voiceDropCount = 0; + _renderingStartTick = 0; + } + + CommandBuffer commandBuffer = new CommandBuffer(commandList, _commandProcessingTimeEstimator); + + CommandGenerator commandGenerator = new CommandGenerator(commandBuffer, GetContext(), _voiceContext, _mixContext, _effectContext, _sinkContext, _splitterContext, _performanceManager); + + _voiceContext.Sort(); + commandGenerator.GenerateVoices(); + + long voicesEstimatedTime = (long)commandBuffer.EstimatedProcessingTime; + + commandGenerator.GenerateSubMixes(); + commandGenerator.GenerateFinalMixes(); + commandGenerator.GenerateSinks(); + + long totalEstimatedTime = (long)commandBuffer.EstimatedProcessingTime; + + if (_voiceDropEnabled) + { + long maxDspTime = GetMaxAllocatedTimeForDsp(); + + long restEstimateTime = totalEstimatedTime - voicesEstimatedTime; + + long deltaTimeDsp = Math.Max(maxDspTime - restEstimateTime, 0); + + _voiceDropCount = ComputeVoiceDrop(commandBuffer, voicesEstimatedTime, deltaTimeDsp); + } + + _voiceContext.UpdateForCommandGeneration(); + + ulong endTicks = GetSystemTicks(); + + _totalElapsedTicks = endTicks - startTicks; + + _renderingStartTick = GetSystemTicks(); + _elapsedFrameCount++; + } + + private int GetMaxAllocatedTimeForDsp() + { + return (int)(Constants.AudioProcessorMaxUpdateTimePerSessions * _behaviourContext.GetAudioRendererProcessingTimeLimit() * (GetRenderingTimeLimit() / 100.0f)); + } + + public void SendCommands() + { + lock (_lock) + { + if (_isActive) + { + _terminationEvent.Reset(); + + GenerateCommandList(out CommandList commands); + + _manager.Processor.Send(_sessionId, + commands, + GetMaxAllocatedTimeForDsp(), + _appletResourceId); + + _systemEvent.Signal(); + } + else + { + _terminationEvent.Set(); + } + } + } + + public uint GetMixBufferCount() + { + return _mixBufferCount; + } + + public void SetRenderingTimeLimitPercent(uint percent) + { + Debug.Assert(percent <= 100); + + _renderingTimeLimitPercent = percent; + } + + public uint GetRenderingTimeLimit() + { + return _renderingTimeLimitPercent; + } + + public Memory<float> GetMixBuffer() + { + return _mixBuffer; + } + + public uint GetSampleCount() + { + return _sampleCount; + } + + public uint GetSampleRate() + { + return _sampleRate; + } + + public uint GetVoiceChannelCountMax() + { + return _voiceChannelCountMax; + } + + public bool IsActive() + { + return _isActive; + } + + private RendererSystemContext GetContext() + { + return new RendererSystemContext + { + ChannelCount = _manager.Processor.OutputDevices[_sessionId].GetChannelCount(), + BehaviourContext = _behaviourContext, + DepopBuffer = _depopBuffer, + MixBufferCount = GetMixBufferCount(), + SessionId = _sessionId, + UpsamplerManager = _upsamplerManager + }; + } + + public int GetSessionId() + { + return _sessionId; + } + + public static ulong GetWorkBufferSize(ref AudioRendererConfiguration parameter) + { + BehaviourContext behaviourContext = new BehaviourContext(); + + behaviourContext.SetUserRevision(parameter.Revision); + + uint mixesCount = parameter.SubMixBufferCount + 1; + + uint memoryPoolCount = parameter.EffectCount + parameter.VoiceCount * Constants.VoiceWaveBufferCount; + + ulong size = 0; + + // Mix Buffers + size = WorkBufferAllocator.GetTargetSize<float>(size, parameter.SampleCount * (Constants.VoiceChannelCountMax + parameter.MixBufferCount), 0x10); + + // Upsampler workbuffer + size = WorkBufferAllocator.GetTargetSize<float>(size, Constants.TargetSampleCount * (Constants.VoiceChannelCountMax + parameter.MixBufferCount) * (parameter.SinkCount + parameter.SubMixBufferCount), 0x10); + + // Depop buffer + size = WorkBufferAllocator.GetTargetSize<float>(size, (ulong)BitUtils.AlignUp(parameter.MixBufferCount, Constants.BufferAlignment), Constants.BufferAlignment); + + // Voice + size = WorkBufferAllocator.GetTargetSize<VoiceState>(size, parameter.VoiceCount, VoiceState.Alignment); + size = WorkBufferAllocator.GetTargetSize<int>(size, parameter.VoiceCount, 0x10); + size = WorkBufferAllocator.GetTargetSize<VoiceChannelResource>(size, parameter.VoiceCount, VoiceChannelResource.Alignment); + size = WorkBufferAllocator.GetTargetSize<VoiceUpdateState>(size, parameter.VoiceCount, VoiceUpdateState.Align); + + // Mix + size = WorkBufferAllocator.GetTargetSize<MixState>(size, mixesCount, MixState.Alignment); + size = WorkBufferAllocator.GetTargetSize<int>(size, parameter.EffectCount * mixesCount, 0x10); + size = WorkBufferAllocator.GetTargetSize<int>(size, mixesCount, 0x10); + + if (behaviourContext.IsSplitterSupported()) + { + size += (ulong)BitUtils.AlignUp(NodeStates.GetWorkBufferSize((int)mixesCount) + EdgeMatrix.GetWorkBufferSize((int)mixesCount), 0x10); + } + + // Memory Pool + size = WorkBufferAllocator.GetTargetSize<MemoryPoolState>(size, memoryPoolCount, MemoryPoolState.Alignment); + + // Splitter + size = SplitterContext.GetWorkBufferSize(size, ref behaviourContext, ref parameter); + + // DSP Voice + size = WorkBufferAllocator.GetTargetSize<VoiceUpdateState>(size, parameter.VoiceCount, VoiceUpdateState.Align); + + // Performance + if (parameter.PerformanceMetricFramesCount > 0) + { + ulong performanceMetricsPerFramesSize = PerformanceManager.GetRequiredBufferSizeForPerformanceMetricsPerFrame(ref parameter, ref behaviourContext) * (parameter.PerformanceMetricFramesCount + 1) + 0xC; + + size += BitUtils.AlignUp(performanceMetricsPerFramesSize, Constants.PerformanceMetricsPerFramesSizeAlignment); + } + + return BitUtils.AlignUp(size, Constants.WorkBufferAlignment); + } + + public ResultCode QuerySystemEvent(out IWritableEvent systemEvent) + { + systemEvent = default; + + if (_executionMode == AudioRendererExecutionMode.Manual) + { + return ResultCode.UnsupportedOperation; + } + + systemEvent = _systemEvent; + + return ResultCode.Success; + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (_isActive) + { + Stop(); + } + + PoolMapper mapper = new PoolMapper(_processHandle, false); + mapper.Unmap(ref _dspMemoryPoolState); + + PoolMapper.ClearUsageState(_memoryPools); + + for (int i = 0; i < _memoryPoolCount; i++) + { + ref MemoryPoolState memoryPool = ref _memoryPools.Span[i]; + + if (memoryPool.IsMapped()) + { + mapper.Unmap(ref memoryPool); + } + } + + _manager.Unregister(this); + _terminationEvent.Dispose(); + _workBufferMemoryPin.Dispose(); + _workBufferRegion.Dispose(); + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/AudioRendererManager.cs b/Ryujinx.Audio/Renderer/Server/AudioRendererManager.cs new file mode 100644 index 00000000..ec847948 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/AudioRendererManager.cs @@ -0,0 +1,334 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Integration; +using Ryujinx.Audio.Renderer.Dsp; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Common.Logging; +using Ryujinx.Memory; +using System; +using System.Diagnostics; +using System.Threading; + +namespace Ryujinx.Audio.Renderer.Server +{ + /// <summary> + /// The audio renderer manager. + /// </summary> + public class AudioRendererManager : IDisposable + { + /// <summary> + /// Lock used for session allocation. + /// </summary> + private object _sessionLock = new object(); + + /// <summary> + /// Lock used to control the <see cref="AudioProcessor"/> running state. + /// </summary> + private object _audioProcessorLock = new object(); + + /// <summary> + /// The session ids allocation table. + /// </summary> + private int[] _sessionIds; + + /// <summary> + /// The events linked to each session. + /// </summary> + private IWritableEvent[] _sessionsSystemEvent; + + /// <summary> + /// The <see cref="AudioRenderSystem"/> sessions instances. + /// </summary> + private AudioRenderSystem[] _sessions; + + /// <summary> + /// The count of active sessions. + /// </summary> + private int _activeSessionCount; + + /// <summary> + /// The worker thread used to run <see cref="SendCommands"/>. + /// </summary> + private Thread _workerThread; + + /// <summary> + /// Indicate if the worker thread and <see cref="AudioProcessor"/> are running. + /// </summary> + private bool _isRunning; + + /// <summary> + /// The audio device driver to create audio outputs. + /// </summary> + private IHardwareDeviceDriver _deviceDriver; + + /// <summary> + /// The <see cref="AudioProcessor"/> instance associated to this manager. + /// </summary> + public AudioProcessor Processor { get; } + + /// <summary> + /// Create a new <see cref="AudioRendererManager"/>. + /// </summary> + public AudioRendererManager() + { + Processor = new AudioProcessor(); + _sessionIds = new int[Constants.AudioRendererSessionCountMax]; + _sessions = new AudioRenderSystem[Constants.AudioRendererSessionCountMax]; + _activeSessionCount = 0; + + for (int i = 0; i < _sessionIds.Length; i++) + { + _sessionIds[i] = i; + } + } + + /// <summary> + /// Initialize the <see cref="AudioRendererManager"/>. + /// </summary> + /// <param name="sessionSystemEvents">The events associated to each session.</param> + /// <param name="deviceDriver">The device driver to use to create audio outputs.</param> + public void Initialize(IWritableEvent[] sessionSystemEvents, IHardwareDeviceDriver deviceDriver) + { + _sessionsSystemEvent = sessionSystemEvents; + _deviceDriver = deviceDriver; + } + + /// <summary> + /// Get the work buffer size required by a session. + /// </summary> + /// <param name="parameter">The user configuration</param> + /// <returns>The work buffer size required by a session.</returns> + public static ulong GetWorkBufferSize(ref AudioRendererConfiguration parameter) + { + return AudioRenderSystem.GetWorkBufferSize(ref parameter); + } + + /// <summary> + /// Acquire a new session id. + /// </summary> + /// <returns>A new session id.</returns> + private int AcquireSessionId() + { + lock (_sessionLock) + { + int index = _activeSessionCount; + + Debug.Assert(index < _sessionIds.Length); + + int sessionId = _sessionIds[index]; + + _sessionIds[index] = -1; + + _activeSessionCount++; + + Logger.Info?.Print(LogClass.AudioRenderer, $"Registered new renderer ({sessionId})"); + + return sessionId; + } + } + + /// <summary> + /// Release a given <paramref name="sessionId"/>. + /// </summary> + /// <param name="sessionId">The session id to release.</param> + private void ReleaseSessionId(int sessionId) + { + lock (_sessionLock) + { + Debug.Assert(_activeSessionCount > 0); + + int newIndex = --_activeSessionCount; + + _sessionIds[newIndex] = sessionId; + } + + Logger.Info?.Print(LogClass.AudioRenderer, $"Unregistered renderer ({sessionId})"); + } + + /// <summary> + /// Check if there is any audio renderer active. + /// </summary> + /// <returns>Returns true if there is any audio renderer active.</returns> + private bool HasAnyActiveRendererLocked() + { + foreach (AudioRenderSystem renderer in _sessions) + { + if (renderer != null) + { + return true; + } + } + + return false; + } + + /// <summary> + /// Start the <see cref="AudioProcessor"/> and worker thread. + /// </summary> + private void StartLocked() + { + _isRunning = true; + + // TODO: virtual device mapping (IAudioDevice) + Processor.Start(_deviceDriver); + + _workerThread = new Thread(SendCommands) + { + Name = "AudioRendererManager.Worker" + }; + + _workerThread.Start(); + } + + /// <summary> + /// Stop the <see cref="AudioProcessor"/> and worker thread. + /// </summary> + private void StopLocked() + { + _isRunning = false; + + _workerThread.Join(); + Processor.Stop(); + + Logger.Info?.Print(LogClass.AudioRenderer, "Stopped audio renderer"); + } + + /// <summary> + /// Worker main function. This is used to dispatch audio renderer commands to the <see cref="AudioProcessor"/>. + /// </summary> + private void SendCommands() + { + Logger.Info?.Print(LogClass.AudioRenderer, "Starting audio renderer"); + Processor.Wait(); + + while (_isRunning) + { + lock (_sessionLock) + { + foreach(AudioRenderSystem renderer in _sessions) + { + renderer?.SendCommands(); + } + } + + Processor.Signal(); + Processor.Wait(); + } + } + + /// <summary> + /// Register a new <see cref="AudioRenderSystem"/>. + /// </summary> + /// <param name="renderer">The <see cref="AudioRenderSystem"/> to register.</param> + private void Register(AudioRenderSystem renderer) + { + lock (_sessionLock) + { + _sessions[renderer.GetSessionId()] = renderer; + } + + lock (_audioProcessorLock) + { + if (!_isRunning) + { + StartLocked(); + } + } + } + + /// <summary> + /// Unregister a new <see cref="AudioRenderSystem"/>. + /// </summary> + /// <param name="renderer">The <see cref="AudioRenderSystem"/> to unregister.</param> + internal void Unregister(AudioRenderSystem renderer) + { + lock (_sessionLock) + { + int sessionId = renderer.GetSessionId(); + + _sessions[renderer.GetSessionId()] = null; + + ReleaseSessionId(sessionId); + } + + lock (_audioProcessorLock) + { + if (_isRunning && !HasAnyActiveRendererLocked()) + { + StopLocked(); + } + } + } + + /// <summary> + /// Open a new <see cref="AudioRenderSystem"/> + /// </summary> + /// <param name="renderer">The new <see cref="AudioRenderSystem"/></param> + /// <param name="memoryManager">The memory manager that will be used for all guest memory operations.</param> + /// <param name="parameter">The user configuration</param> + /// <param name="appletResourceUserId">The applet resource user id of the application.</param> + /// <param name="workBufferAddress">The guest work buffer address.</param> + /// <param name="workBufferSize">The guest work buffer size.</param> + /// <param name="processHandle">The process handle of the application.</param> + /// <returns>A <see cref="ResultCode"/> reporting an error or a success.</returns> + public ResultCode OpenAudioRenderer(out AudioRenderSystem renderer, IVirtualMemoryManager memoryManager, ref AudioRendererConfiguration parameter, ulong appletResourceUserId, ulong workBufferAddress, ulong workBufferSize, uint processHandle) + { + int sessionId = AcquireSessionId(); + + AudioRenderSystem audioRenderer = new AudioRenderSystem(this, _sessionsSystemEvent[sessionId]); + + ResultCode result = audioRenderer.Initialize(ref parameter, processHandle, workBufferAddress, workBufferSize, sessionId, appletResourceUserId, memoryManager); + + if (result == ResultCode.Success) + { + renderer = audioRenderer; + + Register(renderer); + } + else + { + ReleaseSessionId(sessionId); + + renderer = null; + } + + return result; + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + lock (_audioProcessorLock) + { + if (_isRunning) + { + StopLocked(); + } + } + + Processor.Dispose(); + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs b/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs new file mode 100644 index 00000000..b31f9e9f --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs @@ -0,0 +1,405 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using System; +using System.Diagnostics; +using static Ryujinx.Audio.Renderer.Common.BehaviourParameter; + +namespace Ryujinx.Audio.Renderer.Server +{ + /// <summary> + /// Behaviour context. + /// </summary> + /// <remarks>This handles features based on the audio renderer revision provided by the user.</remarks> + public class BehaviourContext + { + /// <summary> + /// The base magic of the Audio Renderer revision. + /// </summary> + public const int BaseRevisionMagic = ('R' << 0) | ('E' << 8) | ('V' << 16) | ('0' << 24); + + /// <summary> + /// REV1: first revision. + /// </summary> + public const int Revision1 = 1 << 24; + + /// <summary> + /// REV2: Added support for splitter and fix GC-ADPCM context not being provided to the DSP. + /// </summary> + /// <remarks>This was added in system update 2.0.0</remarks> + public const int Revision2 = 2 << 24; + + /// <summary> + /// REV3: Incremented the max pre-delay from 150 to 350 for the reverb command and removed the (unused) codec system. + /// </summary> + /// <remarks>This was added in system update 3.0.0</remarks> + public const int Revision3 = 3 << 24; + + /// <summary> + /// REV4: Added USB audio device support and incremented the rendering limit percent to 75%. + /// </summary> + /// <remarks>This was added in system update 4.0.0</remarks> + public const int Revision4 = 4 << 24; + + /// <summary> + /// REV5: <see cref="Parameter.VoiceInParameter.DecodingBehaviour"/>, <see cref="Parameter.VoiceInParameter.FlushWaveBufferCount"/> were added to voice. + /// A new performance frame format (version 2) was added with support for more information about DSP timing. + /// <see cref="Parameter.RendererInfoOutStatus"/> was added to supply the count of update done sent to the DSP. + /// A new version of the command estimator was added to address timing changes caused by the voice changes. + /// Additionally, the rendering limit percent was incremented to 80%. + /// + /// </summary> + /// <remarks>This was added in system update 6.0.0</remarks> + public const int Revision5 = 5 << 24; + + /// <summary> + /// REV6: This fixed a bug in the biquad filter command not clearing up <see cref="Dsp.State.BiquadFilterState"/> with <see cref="Effect.UsageState.New"/> usage state. + /// </summary> + /// <remarks>This was added in system update 6.1.0</remarks> + public const int Revision6 = 6 << 24; + + /// <summary> + /// REV7: Client side (finally) doesn't send all the mix client state to the server and can do partial updates. + /// </summary> + /// <remarks>This was added in system update 8.0.0</remarks> + public const int Revision7 = 7 << 24; + + /// <summary> + /// REV8: + /// Wavebuffer was changed to support more control over loop (you can now specify where to start and end a loop, and how many times to loop). + /// <see cref="Parameter.VoiceInParameter.SrcQuality"/> was added (see <see cref="Parameter.VoiceInParameter.SampleRateConversionQuality"/> for more info). + /// Final leftovers of the codec system were removed. + /// <see cref="Common.SampleFormat.PcmFloat"/> support was added. + /// A new version of the command estimator was added to address timing changes caused by the voice and command changes. + /// </summary> + /// <remarks>This was added in system update 9.0.0</remarks> + public const int Revision8 = 8 << 24; + + /// <summary> + /// Last revision supported by the implementation. + /// </summary> + public const int LastRevision = Revision8; + + /// <summary> + /// Target revision magic supported by the implementation. + /// </summary> + public const int ProcessRevision = BaseRevisionMagic + LastRevision; + + /// <summary> + /// Get the revision number from the revision magic. + /// </summary> + /// <param name="revision">The revision magic.</param> + /// <returns>The revision number.</returns> + public static int GetRevisionNumber(int revision) => (revision - BaseRevisionMagic) >> 24; + + /// <summary> + /// Current active revision. + /// </summary> + public int UserRevision { get; private set; } + + /// <summary> + /// Error storage. + /// </summary> + private ErrorInfo[] _errorInfos; + + /// <summary> + /// Current position in the <see cref="_errorInfos"/> array. + /// </summary> + private uint _errorIndex; + + /// <summary> + /// Current flags of the <see cref="BehaviourContext"/>. + /// </summary> + private ulong _flags; + + /// <summary> + /// Create a new instance of <see cref="BehaviourContext"/>. + /// </summary> + public BehaviourContext() + { + UserRevision = 0; + _errorInfos = new ErrorInfo[Constants.MaxErrorInfos]; + _errorIndex = 0; + } + + /// <summary> + /// Set the active revision. + /// </summary> + /// <param name="userRevision">The active revision.</param> + public void SetUserRevision(int userRevision) + { + UserRevision = userRevision; + } + + /// <summary> + /// Update flags of the <see cref="BehaviourContext"/>. + /// </summary> + /// <param name="flags">The new flags.</param> + public void UpdateFlags(ulong flags) + { + _flags = flags; + } + + /// <summary> + /// Check if a given revision is valid/supported. + /// </summary> + /// <param name="revision">The revision magic to check.</param> + /// <returns>Returns true if the given revision is valid/supported</returns> + public static bool CheckValidRevision(int revision) + { + return GetRevisionNumber(revision) <= GetRevisionNumber(ProcessRevision); + } + + /// <summary> + /// Check if the given revision is greater than or equal the supported revision. + /// </summary> + /// <param name="revision">The revision magic to check.</param> + /// <param name="supportedRevision">The revision magic of the supported revision.</param> + /// <returns>Returns true if the given revision is greater than or equal the supported revision.</returns> + public static bool CheckFeatureSupported(int revision, int supportedRevision) + { + int revA = GetRevisionNumber(revision); + int revB = GetRevisionNumber(supportedRevision); + + if (revA > LastRevision) + { + revA = 1; + } + + if (revB > LastRevision) + { + revB = 1; + } + + return revA >= revB; + } + + /// <summary> + /// Check if the memory pool mapping bypass flag is active. + /// </summary> + /// <returns>True if the memory pool mapping bypass flag is active.</returns> + public bool IsMemoryPoolForceMappingEnabled() + { + return (_flags & 1) != 0; + } + + /// <summary> + /// Check if the audio renderer should fix the GC-ADPCM context not being provided to the DSP. + /// </summary> + /// <returns>True if if the audio renderer should fix it.</returns> + public bool IsAdpcmLoopContextBugFixed() + { + return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision2); + } + + /// <summary> + /// Check if the audio renderer should accept splitters. + /// </summary> + /// <returns>True if the audio renderer should accept splitters.</returns> + public bool IsSplitterSupported() + { + return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision2); + } + + /// <summary> + /// Check if the audio renderer should use a max pre-delay of 350 instead of 150. + /// </summary> + /// <returns>True if the max pre-delay must be 350.</returns> + public bool IsLongSizePreDelaySupported() + { + return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision3); + } + + /// <summary> + /// Check if the audio renderer should expose USB audio device. + /// </summary> + /// <returns>True if the audio renderer should expose USB audio device.</returns> + public bool IsAudioUsbDeviceOutputSupported() + { + return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision4); + } + + /// <summary> + /// Get the percentage allocated to the audio renderer on the DSP for processing. + /// </summary> + /// <returns>The percentage allocated to the audio renderer on the DSP for processing.</returns> + public float GetAudioRendererProcessingTimeLimit() + { + if (CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision5)) + { + return 0.80f; + } + else if (CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision4)) + { + return 0.75f; + } + + return 0.70f; + } + + /// <summary> + /// Check if the audio render should support voice flushing. + /// </summary> + /// <returns>True if the audio render should support voice flushing.</returns> + public bool IsFlushVoiceWaveBuffersSupported() + { + return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision5); + } + + /// <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})"/>. + /// </summary> + /// <returns>True if the audio renderer should trust the user destination count.</returns> + public bool IsSplitterBugFixed() + { + return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision5); + } + + /// <summary> + /// Check if the audio renderer should supply the elapsed frame count to the user when updating. + /// </summary> + /// <returns>True if the audio renderer should supply the elapsed frame count to the user when updating.</returns> + public bool IsElapsedFrameCountSupported() + { + return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision5); + } + + /// <summary> + /// Get the performance metric data format version. + /// </summary> + /// <returns>The performance metric data format version.</returns> + public uint GetPerformanceMetricsDataFormat() + { + if (CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision5)) + { + return 2; + } + else + { + return 1; + } + } + + /// <summary> + /// Check if the audio renderer should support <see cref="Parameter.VoiceInParameter.DecodingBehaviour"/>. + /// </summary> + /// <returns>True if the audio renderer should support <see cref="Parameter.VoiceInParameter.DecodingBehaviour"/>.</returns> + public bool IsDecodingBehaviourFlagSupported() + { + return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision5); + } + + /// <summary> + /// Check if the audio renderer should fix the biquad filter command not clearing up <see cref="Dsp.State.BiquadFilterState"/> with <see cref="Effect.UsageState.New"/> usage state. + /// </summary> + /// <returns>True if the biquad filter state should be cleared.</returns> + public bool IsBiquadFilterEffectStateClearBugFixed() + { + return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision6); + } + + /// <summary> + /// Check if the audio renderer should accept partial mix updates. + /// </summary> + /// <returns>True if the audio renderer should accept partial mix updates.</returns> + public bool IsMixInParameterDirtyOnlyUpdateSupported() + { + return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision7); + } + + /// <summary> + /// Check if the audio renderer should use the new wavebuffer format. + /// </summary> + /// <returns>True if the audio renderer should use the new wavebuffer format.</returns> + public bool IsWaveBufferVersion2Supported() + { + return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision8); + } + + /// <summary> + /// Get the version of the <see cref="ICommandProcessingTimeEstimator"/>. + /// </summary> + /// <returns>The version of the <see cref="ICommandProcessingTimeEstimator"/>.</returns> + public int GetCommandProcessingTimeEstimatorVersion() + { + if (CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision8)) + { + return 3; + } + + if (CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision5)) + { + return 2; + } + + return 1; + } + + /// <summary> + /// Append a new <see cref="ErrorInfo"/> to the error array. + /// </summary> + /// <param name="errorInfo">The new <see cref="ErrorInfo"/> to add.</param> + public void AppendError(ref ErrorInfo errorInfo) + { + Debug.Assert(errorInfo.ErrorCode == ResultCode.Success); + + if (_errorIndex <= Constants.MaxErrorInfos - 1) + { + _errorInfos[_errorIndex++] = errorInfo; + } + } + + /// <summary> + /// Copy the internal <see cref="ErrorInfo"/> array to the given <see cref="Span{ErrorInfo}"/> and output the count copied. + /// </summary> + /// <param name="errorInfos">The output <see cref="Span{ErrorInfo}"/>.</param> + /// <param name="errorCount">The output error count containing the count of <see cref="ErrorInfo"/> copied.</param> + public void CopyErrorInfo(Span<ErrorInfo> errorInfos, out uint errorCount) + { + if (errorInfos.Length != Constants.MaxErrorInfos) + { + throw new ArgumentException("Invalid size of errorInfos span!"); + } + + errorCount = Math.Min(_errorIndex, Constants.MaxErrorInfos); + + for (int i = 0; i < Constants.MaxErrorInfos; i++) + { + if (i < errorCount) + { + errorInfos[i] = _errorInfos[i]; + } + else + { + errorInfos[i] = new ErrorInfo + { + ErrorCode = 0, + ExtraErrorInfo = 0 + }; + } + } + } + + /// <summary> + /// Clear the <see cref="ErrorInfo"/> array. + /// </summary> + public void ClearError() + { + _errorIndex = 0; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/CommandBuffer.cs b/Ryujinx.Audio/Renderer/Server/CommandBuffer.cs new file mode 100644 index 00000000..ca37e090 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/CommandBuffer.cs @@ -0,0 +1,484 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Dsp.Command; +using Ryujinx.Audio.Renderer.Dsp.State; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Parameter.Effect; +using Ryujinx.Audio.Renderer.Server.Performance; +using Ryujinx.Audio.Renderer.Server.Sink; +using Ryujinx.Audio.Renderer.Server.Upsampler; +using Ryujinx.Audio.Renderer.Server.Voice; +using System; +using CpuAddress = System.UInt64; + +namespace Ryujinx.Audio.Renderer.Server +{ + /// <summary> + /// An API to generate commands and aggregate them into a <see cref="CommandList"/>. + /// </summary> + public class CommandBuffer + { + /// <summary> + /// The command processing time estimator in use. + /// </summary> + private ICommandProcessingTimeEstimator _commandProcessingTimeEstimator; + + /// <summary> + /// The estimated total processing time. + /// </summary> + public ulong EstimatedProcessingTime { get; set; } + + /// <summary> + /// The command list that is populated by the <see cref="CommandBuffer"/>. + /// </summary> + public CommandList CommandList { get; } + + /// <summary> + /// Create a new <see cref="CommandBuffer"/>. + /// </summary> + /// <param name="commandList">The command list that will store the generated commands.</param> + /// <param name="commandProcessingTimeEstimator">The command processing time estimator to use.</param> + public CommandBuffer(CommandList commandList, ICommandProcessingTimeEstimator commandProcessingTimeEstimator) + { + CommandList = commandList; + EstimatedProcessingTime = 0; + _commandProcessingTimeEstimator = commandProcessingTimeEstimator; + } + + /// <summary> + /// Add a new generated command to the <see cref="CommandList"/>. + /// </summary> + /// <param name="command">The command to add.</param> + private void AddCommand(ICommand command) + { + EstimatedProcessingTime += command.EstimatedProcessingTime; + + CommandList.AddCommand(command); + } + + /// <summary> + /// Generate a new <see cref="ClearMixBufferCommand"/>. + /// </summary> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateClearMixBuffer(int nodeId) + { + ClearMixBufferCommand command = new ClearMixBufferCommand(nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Generate a new <see cref="DepopPrepareCommand"/>. + /// </summary> + /// <param name="state">The voice state associated.</param> + /// <param name="depopBuffer">The depop buffer.</param> + /// <param name="bufferCount">The buffer count.</param> + /// <param name="bufferOffset">The target buffer offset.</param> + /// <param name="nodeId">The node id associated to this command.</param> + /// <param name="wasPlaying">Set to true if the voice was playing previously.</param> + public void GenerateDepopPrepare(Memory<VoiceUpdateState> state, Memory<float> depopBuffer, uint bufferCount, uint bufferOffset, int nodeId, bool wasPlaying) + { + DepopPrepareCommand command = new DepopPrepareCommand(state, depopBuffer, bufferCount, bufferOffset, nodeId, wasPlaying); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Generate a new <see cref="PerformanceCommand"/>. + /// </summary> + /// <param name="performanceEntryAddresses">The <see cref="PerformanceEntryAddresses"/>.</param> + /// <param name="type">The performance operation to perform.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GeneratePerformance(ref PerformanceEntryAddresses performanceEntryAddresses, PerformanceCommand.Type type, int nodeId) + { + PerformanceCommand command = new PerformanceCommand(ref performanceEntryAddresses, type, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Create a new <see cref="VolumeRampCommand"/>. + /// </summary> + /// <param name="previousVolume">The previous volume.</param> + /// <param name="volume">The new volume.</param> + /// <param name="bufferIndex">The index of the mix buffer to use.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateVolumeRamp(float previousVolume, float volume, uint bufferIndex, int nodeId) + { + VolumeRampCommand command = new VolumeRampCommand(previousVolume, volume, bufferIndex, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Create a new <see cref="DataSourceVersion2Command"/>. + /// </summary> + /// <param name="voiceState">The <see cref="VoiceState"/> to generate the command from.</param> + /// <param name="state">The <see cref="VoiceUpdateState"/> to generate the command from.</param> + /// <param name="outputBufferIndex">The output buffer index to use.</param> + /// <param name="channelIndex">The target channel index.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateDataSourceVersion2(ref VoiceState voiceState, Memory<VoiceUpdateState> state, ushort outputBufferIndex, ushort channelIndex, int nodeId) + { + DataSourceVersion2Command command = new DataSourceVersion2Command(ref voiceState, state, outputBufferIndex, channelIndex, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Create a new <see cref="PcmInt16DataSourceCommandVersion1"/>. + /// </summary> + /// <param name="voiceState">The <see cref="VoiceState"/> to generate the command from.</param> + /// <param name="state">The <see cref="VoiceUpdateState"/> to generate the command from.</param> + /// <param name="outputBufferIndex">The output buffer index to use.</param> + /// <param name="channelIndex">The target channel index.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GeneratePcmInt16DataSourceVersion1(ref VoiceState voiceState, Memory<VoiceUpdateState> state, ushort outputBufferIndex, ushort channelIndex, int nodeId) + { + PcmInt16DataSourceCommandVersion1 command = new PcmInt16DataSourceCommandVersion1(ref voiceState, state, outputBufferIndex, channelIndex, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Create a new <see cref="PcmFloatDataSourceCommandVersion1"/>. + /// </summary> + /// <param name="voiceState">The <see cref="VoiceState"/> to generate the command from.</param> + /// <param name="state">The <see cref="VoiceUpdateState"/> to generate the command from.</param> + /// <param name="outputBufferIndex">The output buffer index to use.</param> + /// <param name="channelIndex">The target channel index.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GeneratePcmFloatDataSourceVersion1(ref VoiceState voiceState, Memory<VoiceUpdateState> state, ushort outputBufferIndex, ushort channelIndex, int nodeId) + { + PcmFloatDataSourceCommandVersion1 command = new PcmFloatDataSourceCommandVersion1(ref voiceState, state, outputBufferIndex, channelIndex, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Create a new <see cref="AdpcmDataSourceCommandVersion1"/>. + /// </summary> + /// <param name="voiceState">The <see cref="VoiceState"/> to generate the command from.</param> + /// <param name="state">The <see cref="VoiceUpdateState"/> to generate the command from.</param> + /// <param name="outputBufferIndex">The output buffer index to use.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateAdpcmDataSourceVersion1(ref VoiceState voiceState, Memory<VoiceUpdateState> state, ushort outputBufferIndex, int nodeId) + { + AdpcmDataSourceCommandVersion1 command = new AdpcmDataSourceCommandVersion1(ref voiceState, state, outputBufferIndex, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Create a new <see cref="BiquadFilterCommand"/>. + /// </summary> + /// <param name="baseIndex">The base index of the input and output buffer.</param> + /// <param name="filter">The biquad filter parameter.</param> + /// <param name="biquadFilterStateMemory">The biquad state.</param> + /// <param name="inputBufferOffset">The input buffer offset.</param> + /// <param name="outputBufferOffset">The output buffer offset.</param> + /// <param name="needInitialization">Set to true if the biquad filter state needs to be initialized.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateBiquadFilter(int baseIndex, ref BiquadFilterParameter filter, Memory<BiquadFilterState> biquadFilterStateMemory, int inputBufferOffset, int outputBufferOffset, bool needInitialization, int nodeId) + { + BiquadFilterCommand command = new BiquadFilterCommand(baseIndex, ref filter, biquadFilterStateMemory, inputBufferOffset, outputBufferOffset, needInitialization, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Generate a new <see cref="MixRampGroupedCommand"/>. + /// </summary> + /// <param name="mixBufferCount">The mix buffer count.</param> + /// <param name="inputBufferIndex">The base input index.</param> + /// <param name="outputBufferIndex">The base output index.</param> + /// <param name="previousVolume">The previous volume.</param> + /// <param name="volume">The new volume.</param> + /// <param name="state">The <see cref="VoiceUpdateState"/> to generate the command from.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateMixRampGrouped(uint mixBufferCount, uint inputBufferIndex, uint outputBufferIndex, Span<float> previousVolume, Span<float> volume, Memory<VoiceUpdateState> state, int nodeId) + { + MixRampGroupedCommand command = new MixRampGroupedCommand(mixBufferCount, inputBufferIndex, outputBufferIndex, previousVolume, volume, state, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Generate a new <see cref="MixRampCommand"/>. + /// </summary> + /// <param name="previousVolume">The previous volume.</param> + /// <param name="volume">The new volume.</param> + /// <param name="inputBufferIndex">The input buffer index.</param> + /// <param name="outputBufferIndex">The output buffer index.</param> + /// <param name="lastSampleIndex">The index in the <see cref="VoiceUpdateState.LastSamples"/> array to store the ramped sample.</param> + /// <param name="state">The <see cref="VoiceUpdateState"/> to generate the command from.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateMixRamp(float previousVolume, float volume, uint inputBufferIndex, uint outputBufferIndex, int lastSampleIndex, Memory<VoiceUpdateState> state, int nodeId) + { + MixRampCommand command = new MixRampCommand(previousVolume, volume, inputBufferIndex, outputBufferIndex, lastSampleIndex, state, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Generate a new <see cref="DepopForMixBuffersCommand"/>. + /// </summary> + /// <param name="depopBuffer">The depop buffer.</param> + /// <param name="bufferOffset">The target buffer offset.</param> + /// <param name="bufferCount">The buffer count.</param> + /// <param name="nodeId">The node id associated to this command.</param> + /// <param name="sampleRate">The target sample rate in use.</param> + public void GenerateDepopForMixBuffersCommand(Memory<float> depopBuffer, uint bufferOffset, uint bufferCount, int nodeId, uint sampleRate) + { + DepopForMixBuffersCommand command = new DepopForMixBuffersCommand(depopBuffer, bufferOffset, bufferCount, nodeId, sampleRate); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Generate a new <see cref="CopyMixBufferCommand"/>. + /// </summary> + /// <param name="inputBufferIndex">The input buffer index.</param> + /// <param name="outputBufferIndex">The output buffer index.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateCopyMixBuffer(uint inputBufferIndex, uint outputBufferIndex, int nodeId) + { + CopyMixBufferCommand command = new CopyMixBufferCommand(inputBufferIndex, outputBufferIndex, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Generate a new <see cref="MixCommand"/>. + /// </summary> + /// <param name="inputBufferIndex">The input buffer index.</param> + /// <param name="outputBufferIndex">The output buffer index.</param> + /// <param name="nodeId">The node id associated to this command.</param> + /// <param name="volume">The mix volume.</param> + public void GenerateMix(uint inputBufferIndex, uint outputBufferIndex, int nodeId, float volume) + { + MixCommand command = new MixCommand(inputBufferIndex, outputBufferIndex, nodeId, volume); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Generate a new <see cref="ReverbCommand"/>. + /// </summary> + /// <param name="bufferOffset">The target buffer offset.</param> + /// <param name="parameter">The reverb parameter.</param> + /// <param name="state">The reverb state.</param> + /// <param name="isEnabled">Set to true if the effect should be active.</param> + /// <param name="workBuffer">The work buffer to use for processing.</param> + /// <param name="nodeId">The node id associated to this command.</param> + /// <param name="isLongSizePreDelaySupported">If set to true, the long size pre-delay is supported.</param> + public void GenerateReverbEffect(uint bufferOffset, ReverbParameter parameter, Memory<ReverbState> state, bool isEnabled, CpuAddress workBuffer, int nodeId, bool isLongSizePreDelaySupported) + { + if (parameter.IsChannelCountValid()) + { + ReverbCommand command = new ReverbCommand(bufferOffset, parameter, state, isEnabled, workBuffer, nodeId, isLongSizePreDelaySupported); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + } + + /// <summary> + /// Generate a new <see cref="Reverb3dCommand"/>. + /// </summary> + /// <param name="bufferOffset">The target buffer offset.</param> + /// <param name="parameter">The reverb 3d parameter.</param> + /// <param name="state">The reverb 3d state.</param> + /// <param name="isEnabled">Set to true if the effect should be active.</param> + /// <param name="workBuffer">The work buffer to use for processing.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateReverb3dEffect(uint bufferOffset, Reverb3dParameter parameter, Memory<Reverb3dState> state, bool isEnabled, CpuAddress workBuffer, int nodeId) + { + if (parameter.IsChannelCountValid()) + { + Reverb3dCommand command = new Reverb3dCommand(bufferOffset, parameter, state, isEnabled, workBuffer, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + } + + + /// <summary> + /// Generate a new <see cref="DelayCommand"/>. + /// </summary> + /// <param name="bufferOffset">The target buffer offset.</param> + /// <param name="parameter">The delay parameter.</param> + /// <param name="state">The delay state.</param> + /// <param name="isEnabled">Set to true if the effect should be active.</param> + /// <param name="workBuffer">The work buffer to use for processing.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateDelayEffect(uint bufferOffset, DelayParameter parameter, Memory<DelayState> state, bool isEnabled, CpuAddress workBuffer, int nodeId) + { + if (parameter.IsChannelCountValid()) + { + DelayCommand command = new DelayCommand(bufferOffset, parameter, state, isEnabled, workBuffer, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + } + + /// <summary> + /// Generate a new <see cref="AuxiliaryBufferCommand"/>. + /// </summary> + /// <param name="bufferOffset">The target buffer offset.</param> + /// <param name="inputBufferOffset">The input buffer offset.</param> + /// <param name="outputBufferOffset">The output buffer offset.</param> + /// <param name="state">The aux state.</param> + /// <param name="isEnabled">Set to true if the effect should be active.</param> + /// <param name="countMax">The limit of the circular buffer.</param> + /// <param name="outputBuffer">The guest address of the output buffer.</param> + /// <param name="inputBuffer">The guest address of the input buffer.</param> + /// <param name="updateCount">The count to add on the offset after write/read operations.</param> + /// <param name="writeOffset">The write offset.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateAuxEffect(uint bufferOffset, byte inputBufferOffset, byte outputBufferOffset, ref AuxiliaryBufferAddresses state, bool isEnabled, uint countMax, CpuAddress outputBuffer, CpuAddress inputBuffer, uint updateCount, uint writeOffset, int nodeId) + { + if (state.SendBufferInfoBase != 0 && state.ReturnBufferInfoBase != 0) + { + AuxiliaryBufferCommand command = new AuxiliaryBufferCommand(bufferOffset, inputBufferOffset, outputBufferOffset, ref state, isEnabled, countMax, outputBuffer, inputBuffer, updateCount, writeOffset, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + } + + /// <summary> + /// Generate a new <see cref="VolumeCommand"/>. + /// </summary> + /// <param name="volume">The target volume to apply.</param> + /// <param name="bufferOffset">The offset of the mix buffer.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateVolume(float volume, uint bufferOffset, int nodeId) + { + VolumeCommand command = new VolumeCommand(volume, bufferOffset, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Create a new <see cref="CircularBufferSinkCommand"/>. + /// </summary> + /// <param name="bufferOffset">The offset of the mix buffer.</param> + /// <param name="sink">The <see cref="BaseSink"/> of the circular buffer.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateCircularBuffer(uint bufferOffset, CircularBufferSink sink, int nodeId) + { + CircularBufferSinkCommand command = new CircularBufferSinkCommand(bufferOffset, ref sink.Parameter, ref sink.CircularBufferAddressInfo, sink.CurrentWriteOffset, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Create a new <see cref="DownMixSurroundToStereoCommand"/>. + /// </summary> + /// <param name="bufferOffset">The offset of the mix buffer.</param> + /// <param name="inputBufferOffset">The input buffer offset.</param> + /// <param name="outputBufferOffset">The output buffer offset.</param> + /// <param name="downMixParameter">The downmixer parameters to use.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateDownMixSurroundToStereo(uint bufferOffset, Span<byte> inputBufferOffset, Span<byte> outputBufferOffset, ReadOnlySpan<float> downMixParameter, int nodeId) + { + DownMixSurroundToStereoCommand command = new DownMixSurroundToStereoCommand(bufferOffset, inputBufferOffset, outputBufferOffset, downMixParameter, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Create a new <see cref="UpsampleCommand"/>. + /// </summary> + /// <param name="bufferOffset">The offset of the mix buffer.</param> + /// <param name="upsampler">The <see cref="UpsamplerState"/> associated.</param> + /// <param name="inputCount">The total input count.</param> + /// <param name="inputBufferOffset">The input buffer mix offset.</param> + /// <param name="bufferCountPerSample">The buffer count per sample.</param> + /// <param name="sampleCount">The source sample count.</param> + /// <param name="sampleRate">The source sample rate.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateUpsample(uint bufferOffset, UpsamplerState upsampler, uint inputCount, Span<byte> inputBufferOffset, uint bufferCountPerSample, uint sampleCount, uint sampleRate, int nodeId) + { + UpsampleCommand command = new UpsampleCommand(bufferOffset, upsampler, inputCount, inputBufferOffset, bufferCountPerSample, sampleCount, sampleRate, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Create a new <see cref="DeviceSinkCommand"/>. + /// </summary> + /// <param name="bufferOffset">The offset of the mix buffer.</param> + /// <param name="sink">The <see cref="BaseSink"/> of the device sink.</param> + /// <param name="sessionId">The current audio renderer session id.</param> + /// <param name="buffer">The mix buffer in use.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateDeviceSink(uint bufferOffset, DeviceSink sink, int sessionId, Memory<float> buffer, int nodeId) + { + DeviceSinkCommand command = new DeviceSinkCommand(bufferOffset, sink, sessionId, buffer, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/CommandGenerator.cs b/Ryujinx.Audio/Renderer/Server/CommandGenerator.cs new file mode 100644 index 00000000..d2499c1d --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/CommandGenerator.cs @@ -0,0 +1,940 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Common; +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Dsp.Command; +using Ryujinx.Audio.Renderer.Dsp.State; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Server.Effect; +using Ryujinx.Audio.Renderer.Server.Mix; +using Ryujinx.Audio.Renderer.Server.Performance; +using Ryujinx.Audio.Renderer.Server.Sink; +using Ryujinx.Audio.Renderer.Server.Splitter; +using Ryujinx.Audio.Renderer.Server.Voice; +using Ryujinx.Audio.Renderer.Utils; +using System; +using System.Diagnostics; + +namespace Ryujinx.Audio.Renderer.Server +{ + public class CommandGenerator + { + private CommandBuffer _commandBuffer; + private RendererSystemContext _rendererContext; + private VoiceContext _voiceContext; + private MixContext _mixContext; + private EffectContext _effectContext; + private SinkContext _sinkContext; + private SplitterContext _splitterContext; + private PerformanceManager _performanceManager; + + public CommandGenerator(CommandBuffer commandBuffer, RendererSystemContext rendererContext, VoiceContext voiceContext, MixContext mixContext, EffectContext effectContext, SinkContext sinkContext, SplitterContext splitterContext, PerformanceManager performanceManager) + { + _commandBuffer = commandBuffer; + _rendererContext = rendererContext; + _voiceContext = voiceContext; + _mixContext = mixContext; + _effectContext = effectContext; + _sinkContext = sinkContext; + _splitterContext = splitterContext; + _performanceManager = performanceManager; + + _commandBuffer.GenerateClearMixBuffer(Constants.InvalidNodeId); + } + + private void GenerateDataSource(ref VoiceState voiceState, Memory<VoiceUpdateState> dspState, int channelIndex) + { + if (voiceState.MixId != Constants.UnusedMixId) + { + ref MixState mix = ref _mixContext.GetState(voiceState.MixId); + + _commandBuffer.GenerateDepopPrepare(dspState, + _rendererContext.DepopBuffer, + mix.BufferCount, + mix.BufferOffset, + voiceState.NodeId, + voiceState.WasPlaying); + } + else if (voiceState.SplitterId != Constants.UnusedSplitterId) + { + int destinationId = 0; + + while (true) + { + Span<SplitterDestination> destinationSpan = _splitterContext.GetDestination((int)voiceState.SplitterId, destinationId++); + + if (destinationSpan.IsEmpty) + { + break; + } + + ref SplitterDestination destination = ref destinationSpan[0]; + + if (destination.IsConfigured()) + { + int mixId = destination.DestinationId; + + if (mixId < _mixContext.GetCount() && mixId != Constants.UnusedSplitterIdInt) + { + ref MixState mix = ref _mixContext.GetState(mixId); + + _commandBuffer.GenerateDepopPrepare(dspState, + _rendererContext.DepopBuffer, + mix.BufferCount, + mix.BufferOffset, + voiceState.NodeId, + voiceState.WasPlaying); + + destination.MarkAsNeedToUpdateInternalState(); + } + } + } + } + + if (!voiceState.WasPlaying) + { + Debug.Assert(voiceState.SampleFormat != SampleFormat.Adpcm || channelIndex == 0); + + if (_rendererContext.BehaviourContext.IsWaveBufferVersion2Supported()) + { + _commandBuffer.GenerateDataSourceVersion2(ref voiceState, + dspState, + (ushort)_rendererContext.MixBufferCount, + (ushort)channelIndex, + voiceState.NodeId); + } + else + { + switch (voiceState.SampleFormat) + { + case SampleFormat.PcmInt16: + _commandBuffer.GeneratePcmInt16DataSourceVersion1(ref voiceState, + dspState, + (ushort)_rendererContext.MixBufferCount, + (ushort)channelIndex, + voiceState.NodeId); + break; + case SampleFormat.PcmFloat: + _commandBuffer.GeneratePcmFloatDataSourceVersion1(ref voiceState, + dspState, + (ushort)_rendererContext.MixBufferCount, + (ushort)channelIndex, + voiceState.NodeId); + break; + case SampleFormat.Adpcm: + _commandBuffer.GenerateAdpcmDataSourceVersion1(ref voiceState, + dspState, + (ushort)_rendererContext.MixBufferCount, + voiceState.NodeId); + break; + default: + throw new NotImplementedException($"Unsupported data source {voiceState.SampleFormat}"); + } + } + } + } + + private void GenerateBiquadFilterForVoice(ref VoiceState voiceState, Memory<VoiceUpdateState> state, int baseIndex, int bufferOffset, int nodeId) + { + for (int i = 0; i < voiceState.BiquadFilters.Length; i++) + { + ref BiquadFilterParameter filter = ref voiceState.BiquadFilters[i]; + + if (filter.Enable) + { + Memory<byte> biquadStateRawMemory = SpanMemoryManager<byte>.Cast(state).Slice(VoiceUpdateState.BiquadStateOffset, VoiceUpdateState.BiquadStateSize * Constants.VoiceBiquadFilterCount); + + Memory<BiquadFilterState> stateMemory = SpanMemoryManager<BiquadFilterState>.Cast(biquadStateRawMemory); + + _commandBuffer.GenerateBiquadFilter(baseIndex, + ref filter, + stateMemory.Slice(i, 1), + bufferOffset, + bufferOffset, + !voiceState.BiquadFilterNeedInitialization[i], + nodeId); + } + } + } + + private void GenerateVoiceMix(Span<float> mixVolumes, Span<float> previousMixVolumes, Memory<VoiceUpdateState> state, uint bufferOffset, uint bufferCount, uint bufferIndex, int nodeId) + { + if (bufferCount > Constants.VoiceChannelCountMax) + { + _commandBuffer.GenerateMixRampGrouped(bufferCount, + bufferIndex, + bufferOffset, + previousMixVolumes, + mixVolumes, + state, + nodeId); + } + else + { + for (int i = 0; i < bufferCount; i++) + { + float previousMixVolume = previousMixVolumes[i]; + float mixVolume = mixVolumes[i]; + + if (mixVolume != 0.0f || previousMixVolume != 0.0f) + { + _commandBuffer.GenerateMixRamp(previousMixVolume, + mixVolume, + bufferIndex, + bufferOffset + (uint)i, + i, + state, + nodeId); + } + } + } + } + + private void GenerateVoice(ref VoiceState voiceState) + { + int nodeId = voiceState.NodeId; + uint channelsCount = voiceState.ChannelsCount; + + for (int channelIndex = 0; channelIndex < channelsCount; channelIndex++) + { + Memory<VoiceUpdateState> dspStateMemory = _voiceContext.GetUpdateStateForDsp(voiceState.ChannelResourceIds[channelIndex]); + + ref VoiceChannelResource channelResource = ref _voiceContext.GetChannelResource(voiceState.ChannelResourceIds[channelIndex]); + + PerformanceDetailType dataSourceDetailType = PerformanceDetailType.Adpcm; + + if (voiceState.SampleFormat == SampleFormat.PcmInt16) + { + dataSourceDetailType = PerformanceDetailType.PcmInt16; + } + else if (voiceState.SampleFormat == SampleFormat.PcmFloat) + { + dataSourceDetailType = PerformanceDetailType.PcmFloat; + } + + bool performanceInitialized = false; + + PerformanceEntryAddresses performanceEntry = new PerformanceEntryAddresses(); + + if (_performanceManager != null && _performanceManager.IsTargetNodeId(nodeId) && _performanceManager.GetNextEntry(out performanceEntry, dataSourceDetailType, PerformanceEntryType.Voice, nodeId)) + { + performanceInitialized = true; + + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId); + } + + GenerateDataSource(ref voiceState, dspStateMemory, channelIndex); + + if (performanceInitialized) + { + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId); + } + + if (voiceState.WasPlaying) + { + voiceState.PreviousVolume = 0.0f; + } + else if (voiceState.HasAnyDestination()) + { + performanceInitialized = false; + + if (_performanceManager != null && _performanceManager.IsTargetNodeId(nodeId) && _performanceManager.GetNextEntry(out performanceEntry, PerformanceDetailType.BiquadFilter, PerformanceEntryType.Voice, nodeId)) + { + performanceInitialized = true; + + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId); + } + + GenerateBiquadFilterForVoice(ref voiceState, dspStateMemory, (int)_rendererContext.MixBufferCount, channelIndex, nodeId); + + if (performanceInitialized) + { + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId); + } + + performanceInitialized = false; + + if (_performanceManager != null && _performanceManager.IsTargetNodeId(nodeId) && _performanceManager.GetNextEntry(out performanceEntry, PerformanceDetailType.VolumeRamp, PerformanceEntryType.Voice, nodeId)) + { + performanceInitialized = true; + + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId); + } + + _commandBuffer.GenerateVolumeRamp(voiceState.PreviousVolume, + voiceState.Volume, + _rendererContext.MixBufferCount + (uint)channelIndex, + nodeId); + + if (performanceInitialized) + { + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId); + } + + voiceState.PreviousVolume = voiceState.Volume; + + if (voiceState.MixId == Constants.UnusedMixId) + { + if (voiceState.SplitterId != Constants.UnusedSplitterId) + { + int destinationId = channelIndex; + + while (true) + { + Span<SplitterDestination> destinationSpan = _splitterContext.GetDestination((int)voiceState.SplitterId, destinationId); + + if (destinationSpan.IsEmpty) + { + break; + } + + ref SplitterDestination destination = ref destinationSpan[0]; + + destinationId += (int)channelsCount; + + if (destination.IsConfigured()) + { + int mixId = destination.DestinationId; + + if (mixId < _mixContext.GetCount() && mixId != Constants.UnusedSplitterIdInt) + { + ref MixState mix = ref _mixContext.GetState(mixId); + + GenerateVoiceMix(destination.MixBufferVolume, + destination.PreviousMixBufferVolume, + dspStateMemory, + mix.BufferOffset, + mix.BufferCount, + _rendererContext.MixBufferCount + (uint)channelIndex, + nodeId); + + destination.MarkAsNeedToUpdateInternalState(); + } + } + } + } + } + else + { + ref MixState mix = ref _mixContext.GetState(voiceState.MixId); + + performanceInitialized = false; + + if (_performanceManager != null && _performanceManager.IsTargetNodeId(nodeId) && _performanceManager.GetNextEntry(out performanceEntry, PerformanceDetailType.Mix, PerformanceEntryType.Voice, nodeId)) + { + performanceInitialized = true; + + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId); + } + + GenerateVoiceMix(channelResource.Mix.ToSpan(), + channelResource.PreviousMix.ToSpan(), + dspStateMemory, + mix.BufferOffset, + mix.BufferCount, + _rendererContext.MixBufferCount + (uint)channelIndex, + nodeId); + + if (performanceInitialized) + { + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId); + } + + channelResource.UpdateState(); + } + + for (int i = 0; i < voiceState.BiquadFilterNeedInitialization.Length; i++) + { + voiceState.BiquadFilterNeedInitialization[i] = voiceState.BiquadFilters[i].Enable; + } + } + } + } + + public void GenerateVoices() + { + for (int i = 0; i < _voiceContext.GetCount(); i++) + { + ref VoiceState sortedState = ref _voiceContext.GetSortedState(i); + + if (!sortedState.ShouldSkip() && sortedState.UpdateForCommandGeneration(_voiceContext)) + { + int nodeId = sortedState.NodeId; + + PerformanceEntryAddresses performanceEntry = new PerformanceEntryAddresses(); + + bool performanceInitialized = false; + + if (_performanceManager != null && _performanceManager.GetNextEntry(out performanceEntry, PerformanceEntryType.Voice, nodeId)) + { + performanceInitialized = true; + + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId); + } + + GenerateVoice(ref sortedState); + + if (performanceInitialized) + { + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId); + } + } + } + + _splitterContext.UpdateInternalState(); + } + + public void GeneratePerformance(ref PerformanceEntryAddresses performanceEntryAddresses, PerformanceCommand.Type type, int nodeId) + { + _commandBuffer.GeneratePerformance(ref performanceEntryAddresses, type, nodeId); + } + + private void GenerateBufferMixerEffect(int bufferOffset, BufferMixEffect effect, int nodeId) + { + Debug.Assert(effect.Type == EffectType.BufferMix); + + if (effect.IsEnabled) + { + for (int i = 0; i < effect.Parameter.MixesCount; i++) + { + if (effect.Parameter.Volumes[i] != 0.0f) + { + _commandBuffer.GenerateMix((uint)bufferOffset + effect.Parameter.Input[i], + (uint)bufferOffset + effect.Parameter.Output[i], + nodeId, + effect.Parameter.Volumes[i]); + } + } + } + } + + private void GenerateAuxEffect(uint bufferOffset, AuxiliaryBufferEffect effect, int nodeId) + { + Debug.Assert(effect.Type == EffectType.AuxiliaryBuffer); + + if (effect.IsEnabled) + { + effect.GetWorkBuffer(0); + effect.GetWorkBuffer(1); + } + + if (effect.State.SendBufferInfoBase != 0 && effect.State.ReturnBufferInfoBase != 0) + { + int i = 0; + uint writeOffset = 0; + for (uint channelIndex = effect.Parameter.ChannelCount; channelIndex != 0; channelIndex--) + { + uint newUpdateCount = writeOffset + _commandBuffer.CommandList.SampleCount; + + uint updateCount; + + if ((channelIndex - 1) != 0) + { + updateCount = 0; + } + else + { + updateCount = newUpdateCount; + } + + _commandBuffer.GenerateAuxEffect(bufferOffset, + effect.Parameter.Input[i], + effect.Parameter.Output[i], + ref effect.State, + effect.IsEnabled, + effect.Parameter.BufferStorageSize, + effect.State.SendBufferInfoBase, + effect.State.ReturnBufferInfoBase, + updateCount, + writeOffset, + nodeId); + + writeOffset = newUpdateCount; + + i++; + } + } + } + + private void GenerateDelayEffect(uint bufferOffset, DelayEffect effect, int nodeId) + { + Debug.Assert(effect.Type == EffectType.Delay); + + ulong workBuffer = effect.GetWorkBuffer(-1); + + _commandBuffer.GenerateDelayEffect(bufferOffset, effect.Parameter, effect.State, effect.IsEnabled, workBuffer, nodeId); + } + + private void GenerateReverbEffect(uint bufferOffset, ReverbEffect effect, int nodeId, bool isLongSizePreDelaySupported) + { + Debug.Assert(effect.Type == EffectType.Reverb); + + ulong workBuffer = effect.GetWorkBuffer(-1); + + _commandBuffer.GenerateReverbEffect(bufferOffset, effect.Parameter, effect.State, effect.IsEnabled, workBuffer, nodeId, isLongSizePreDelaySupported); + } + + private void GenerateReverb3dEffect(uint bufferOffset, Reverb3dEffect effect, int nodeId) + { + Debug.Assert(effect.Type == EffectType.Reverb3d); + + ulong workBuffer = effect.GetWorkBuffer(-1); + + _commandBuffer.GenerateReverb3dEffect(bufferOffset, effect.Parameter, effect.State, effect.IsEnabled, workBuffer, nodeId); + } + + private void GenerateBiquadFilterEffect(uint bufferOffset, BiquadFilterEffect effect, int nodeId) + { + Debug.Assert(effect.Type == EffectType.BiquadFilter); + + if (effect.IsEnabled) + { + bool needInitialization = effect.Parameter.Status == UsageState.Invalid || + (effect.Parameter.Status == UsageState.New && !_rendererContext.BehaviourContext.IsBiquadFilterEffectStateClearBugFixed()); + + BiquadFilterParameter parameter = new BiquadFilterParameter(); + + parameter.Enable = true; + effect.Parameter.Denominator.ToSpan().CopyTo(parameter.Denominator.ToSpan()); + effect.Parameter.Numerator.ToSpan().CopyTo(parameter.Numerator.ToSpan()); + + for (int i = 0; i < effect.Parameter.ChannelCount; i++) + { + _commandBuffer.GenerateBiquadFilter((int)bufferOffset, ref parameter, effect.State.Slice(i, 1), + effect.Parameter.Input[i], + effect.Parameter.Output[i], + needInitialization, + nodeId); + } + } + else + { + for (int i = 0; i < effect.Parameter.ChannelCount; i++) + { + uint inputBufferIndex = bufferOffset + effect.Parameter.Input[i]; + uint outputBufferIndex = bufferOffset + effect.Parameter.Output[i]; + + // If the input and output isn't the same, generate a command. + if (inputBufferIndex != outputBufferIndex) + { + _commandBuffer.GenerateCopyMixBuffer(inputBufferIndex, outputBufferIndex, nodeId); + } + } + } + } + + private void GenerateEffect(ref MixState mix, BaseEffect effect) + { + int nodeId = mix.NodeId; + + bool isFinalMix = mix.MixId == Constants.FinalMixId; + + PerformanceEntryAddresses performanceEntry = new PerformanceEntryAddresses(); + + bool performanceInitialized = false; + + if (_performanceManager != null && _performanceManager.GetNextEntry(out performanceEntry, effect.GetPerformanceDetailType(), + isFinalMix ? PerformanceEntryType.FinalMix : PerformanceEntryType.SubMix, nodeId)) + { + performanceInitialized = true; + + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId); + } + + switch (effect.Type) + { + case EffectType.BufferMix: + GenerateBufferMixerEffect((int)mix.BufferOffset, (BufferMixEffect)effect, nodeId); + break; + case EffectType.AuxiliaryBuffer: + GenerateAuxEffect(mix.BufferOffset, (AuxiliaryBufferEffect)effect, nodeId); + break; + case EffectType.Delay: + GenerateDelayEffect(mix.BufferOffset, (DelayEffect)effect, nodeId); + break; + case EffectType.Reverb: + GenerateReverbEffect(mix.BufferOffset, (ReverbEffect)effect, nodeId, mix.IsLongSizePreDelaySupported); + break; + case EffectType.Reverb3d: + GenerateReverb3dEffect(mix.BufferOffset, (Reverb3dEffect)effect, nodeId); + break; + case EffectType.BiquadFilter: + GenerateBiquadFilterEffect(mix.BufferOffset, (BiquadFilterEffect)effect, nodeId); + break; + default: + throw new NotImplementedException($"Unsupported effect type {effect.Type}"); + } + + if (performanceInitialized) + { + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId); + } + + effect.UpdateForCommandGeneration(); + } + + private void GenerateEffects(ref MixState mix) + { + ReadOnlySpan<int> effectProcessingOrderArray = mix.EffectProcessingOrderArray; + + Debug.Assert(_effectContext.GetCount() == 0 || !effectProcessingOrderArray.IsEmpty); + + for (int i = 0; i < _effectContext.GetCount(); i++) + { + int effectOrder = effectProcessingOrderArray[i]; + + if (effectOrder == Constants.InvalidProcessingOrder) + { + break; + } + + // BaseEffect is a class, we don't need to pass it by ref + BaseEffect effect = _effectContext.GetEffect(effectOrder); + + Debug.Assert(effect.Type != EffectType.Invalid); + Debug.Assert(effect.MixId == mix.MixId); + + if (!effect.ShouldSkip()) + { + GenerateEffect(ref mix, effect); + } + } + } + + private void GenerateMix(ref MixState mix) + { + if (mix.HasAnyDestination()) + { + Debug.Assert(mix.DestinationMixId != Constants.UnusedMixId || mix.DestinationSplitterId != Constants.UnusedSplitterId); + + if (mix.DestinationMixId == Constants.UnusedMixId) + { + if (mix.DestinationSplitterId != Constants.UnusedSplitterId) + { + int destinationId = 0; + + while (true) + { + int destinationIndex = destinationId++; + + Span<SplitterDestination> destinationSpan = _splitterContext.GetDestination((int)mix.DestinationSplitterId, destinationIndex); + + if (destinationSpan.IsEmpty) + { + break; + } + + ref SplitterDestination destination = ref destinationSpan[0]; + + if (destination.IsConfigured()) + { + int mixId = destination.DestinationId; + + if (mixId < _mixContext.GetCount() && mixId != Constants.UnusedSplitterIdInt) + { + ref MixState destinationMix = ref _mixContext.GetState(mixId); + + uint inputBufferIndex = mix.BufferOffset + ((uint)destinationIndex % mix.BufferCount); + + for (uint bufferDestinationIndex = 0; bufferDestinationIndex < destinationMix.BufferCount; bufferDestinationIndex++) + { + float volume = mix.Volume * destination.GetMixVolume((int)bufferDestinationIndex); + + if (volume != 0.0f) + { + _commandBuffer.GenerateMix(inputBufferIndex, + destinationMix.BufferOffset + bufferDestinationIndex, + mix.NodeId, + volume); + } + } + } + } + } + } + } + else + { + ref MixState destinationMix = ref _mixContext.GetState(mix.DestinationMixId); + + for (uint bufferIndex = 0; bufferIndex < mix.BufferCount; bufferIndex++) + { + for (uint bufferDestinationIndex = 0; bufferDestinationIndex < destinationMix.BufferCount; bufferDestinationIndex++) + { + float volume = mix.Volume * mix.GetMixBufferVolume((int)bufferIndex, (int)bufferDestinationIndex); + + if (volume != 0.0f) + { + _commandBuffer.GenerateMix(mix.BufferOffset + bufferIndex, + destinationMix.BufferOffset + bufferDestinationIndex, + mix.NodeId, + volume); + } + } + } + } + } + } + + private void GenerateSubMix(ref MixState subMix) + { + _commandBuffer.GenerateDepopForMixBuffersCommand(_rendererContext.DepopBuffer, + subMix.BufferOffset, + subMix.BufferCount, + subMix.NodeId, + subMix.SampleRate); + + GenerateEffects(ref subMix); + + PerformanceEntryAddresses performanceEntry = new PerformanceEntryAddresses(); + + int nodeId = subMix.NodeId; + + bool performanceInitialized = false; + + if (_performanceManager != null && _performanceManager.IsTargetNodeId(nodeId) && _performanceManager.GetNextEntry(out performanceEntry, PerformanceDetailType.Mix, PerformanceEntryType.SubMix, nodeId)) + { + performanceInitialized = true; + + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId); + } + + GenerateMix(ref subMix); + + if (performanceInitialized) + { + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId); + } + } + + public void GenerateSubMixes() + { + for (int id = 0; id < _mixContext.GetCount(); id++) + { + ref MixState sortedState = ref _mixContext.GetSortedState(id); + + if (sortedState.IsUsed && sortedState.MixId != Constants.FinalMixId) + { + int nodeId = sortedState.NodeId; + + PerformanceEntryAddresses performanceEntry = new PerformanceEntryAddresses(); + + bool performanceInitialized = false; + + if (_performanceManager != null && _performanceManager.GetNextEntry(out performanceEntry, PerformanceEntryType.SubMix, nodeId)) + { + performanceInitialized = true; + + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId); + } + + GenerateSubMix(ref sortedState); + + if (performanceInitialized) + { + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId); + } + } + } + } + + private void GenerateFinalMix() + { + ref MixState finalMix = ref _mixContext.GetFinalState(); + + _commandBuffer.GenerateDepopForMixBuffersCommand(_rendererContext.DepopBuffer, + finalMix.BufferOffset, + finalMix.BufferCount, + finalMix.NodeId, + finalMix.SampleRate); + + GenerateEffects(ref finalMix); + + PerformanceEntryAddresses performanceEntry = new PerformanceEntryAddresses(); + + int nodeId = finalMix.NodeId; + + bool performanceInitialized = false; + + if (_performanceManager != null && _performanceManager.IsTargetNodeId(nodeId) && _performanceManager.GetNextEntry(out performanceEntry, PerformanceDetailType.Mix, PerformanceEntryType.FinalMix, nodeId)) + { + performanceInitialized = true; + + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId); + } + + // Only generate volume command if the volume isn't 100%. + if (finalMix.Volume != 1.0f) + { + for (uint bufferIndex = 0; bufferIndex < finalMix.BufferCount; bufferIndex++) + { + bool performanceSubInitialized = false; + + if (_performanceManager != null && _performanceManager.IsTargetNodeId(nodeId) && _performanceManager.GetNextEntry(out performanceEntry, PerformanceDetailType.VolumeRamp, PerformanceEntryType.FinalMix, nodeId)) + { + performanceSubInitialized = true; + + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId); + } + + _commandBuffer.GenerateVolume(finalMix.Volume, + finalMix.BufferOffset + bufferIndex, + nodeId); + + if (performanceSubInitialized) + { + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId); + } + } + } + + if (performanceInitialized) + { + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId); + } + } + + public void GenerateFinalMixes() + { + int nodeId = _mixContext.GetFinalState().NodeId; + + PerformanceEntryAddresses performanceEntry = new PerformanceEntryAddresses(); + + bool performanceInitialized = false; + + if (_performanceManager != null && _performanceManager.GetNextEntry(out performanceEntry, PerformanceEntryType.FinalMix, nodeId)) + { + performanceInitialized = true; + + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId); + } + + GenerateFinalMix(); + + if (performanceInitialized) + { + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId); + } + } + + private void GenerateCircularBuffer(CircularBufferSink sink, ref MixState finalMix) + { + _commandBuffer.GenerateCircularBuffer(finalMix.BufferOffset, sink, Constants.InvalidNodeId); + } + + private void GenerateDevice(DeviceSink sink, ref MixState finalMix) + { + if (_commandBuffer.CommandList.SampleRate != 48000 && sink.UpsamplerState == null) + { + sink.UpsamplerState = _rendererContext.UpsamplerManager.Allocate(); + } + + bool useCustomDownMixingCommand = _rendererContext.ChannelCount == 2 && sink.Parameter.DownMixParameterEnabled; + + if (useCustomDownMixingCommand) + { + _commandBuffer.GenerateDownMixSurroundToStereo(finalMix.BufferOffset, + sink.Parameter.Input.ToSpan(), + sink.Parameter.Input.ToSpan(), + sink.DownMixCoefficients, + Constants.InvalidNodeId); + } + // NOTE: We do the downmixing at the DSP level as it's easier that way. + else if (_rendererContext.ChannelCount == 2 && sink.Parameter.InputCount == 6) + { + _commandBuffer.GenerateDownMixSurroundToStereo(finalMix.BufferOffset, + sink.Parameter.Input.ToSpan(), + sink.Parameter.Input.ToSpan(), + Constants.DefaultSurroundToStereoCoefficients, + Constants.InvalidNodeId); + } + + CommandList commandList = _commandBuffer.CommandList; + + if (sink.UpsamplerState != null) + { + _commandBuffer.GenerateUpsample(finalMix.BufferOffset, + sink.UpsamplerState, + sink.Parameter.InputCount, + sink.Parameter.Input.ToSpan(), + commandList.BufferCount, + commandList.SampleCount, + commandList.SampleRate, + Constants.InvalidNodeId); + } + + _commandBuffer.GenerateDeviceSink(finalMix.BufferOffset, + sink, + _rendererContext.SessionId, + commandList.Buffers, + Constants.InvalidNodeId); + } + + private void GenerateSink(BaseSink sink, ref MixState finalMix) + { + bool performanceInitialized = false; + + PerformanceEntryAddresses performanceEntry = new PerformanceEntryAddresses(); + + if (_performanceManager != null && _performanceManager.GetNextEntry(out performanceEntry, PerformanceEntryType.Sink, sink.NodeId)) + { + performanceInitialized = true; + + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, sink.NodeId); + } + + if (!sink.ShouldSkip) + { + switch (sink.Type) + { + case SinkType.CircularBuffer: + GenerateCircularBuffer((CircularBufferSink)sink, ref finalMix); + break; + case SinkType.Device: + GenerateDevice((DeviceSink)sink, ref finalMix); + break; + default: + throw new NotImplementedException($"Unsupported sink type {sink.Type}"); + } + + sink.UpdateForCommandGeneration(); + } + + if (performanceInitialized) + { + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, sink.NodeId); + } + } + + public void GenerateSinks() + { + ref MixState finalMix = ref _mixContext.GetFinalState(); + + for (int i = 0; i < _sinkContext.GetCount(); i++) + { + // BaseSink is a class, we don't need to pass it by ref + BaseSink sink = _sinkContext.GetSink(i); + + if (sink.IsUsed) + { + GenerateSink(sink, ref finalMix); + } + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion1.cs b/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion1.cs new file mode 100644 index 00000000..81d3b57b --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion1.cs @@ -0,0 +1,180 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Dsp.Command; +using System.Diagnostics; + +namespace Ryujinx.Audio.Renderer.Server +{ + /// <summary> + /// <see cref="ICommandProcessingTimeEstimator"/> version 1. + /// </summary> + public class CommandProcessingTimeEstimatorVersion1 : ICommandProcessingTimeEstimator + { + private uint _sampleCount; + private uint _bufferCount; + + public CommandProcessingTimeEstimatorVersion1(uint sampleCount, uint bufferCount) + { + _sampleCount = sampleCount; + _bufferCount = bufferCount; + } + + public uint Estimate(PerformanceCommand command) + { + return 1454; + } + + public uint Estimate(ClearMixBufferCommand command) + { + return (uint)(_sampleCount * 0.83f * _bufferCount * 1.2f); + } + + public uint Estimate(BiquadFilterCommand command) + { + return (uint)(_sampleCount * 58.0f * 1.2f); + } + + public uint Estimate(MixRampGroupedCommand command) + { + int volumeCount = 0; + + for (int i = 0; i < command.MixBufferCount; i++) + { + if (command.Volume0[i] != 0.0f || command.Volume1[i] != 0.0f) + { + volumeCount++; + } + } + + return (uint)(_sampleCount * 14.4f * 1.2f * volumeCount); + } + + public uint Estimate(MixRampCommand command) + { + return (uint)(_sampleCount * 14.4f * 1.2f); + } + + public uint Estimate(DepopPrepareCommand command) + { + return 1080; + } + + public uint Estimate(VolumeRampCommand command) + { + return (uint)(_sampleCount * 9.8f * 1.2f); + } + + public uint Estimate(PcmInt16DataSourceCommandVersion1 command) + { + return (uint)(command.Pitch * 0.25f * 1.2f); + } + + public uint Estimate(AdpcmDataSourceCommandVersion1 command) + { + return (uint)(command.Pitch * 0.46f * 1.2f); + } + + public uint Estimate(DepopForMixBuffersCommand command) + { + return (uint)(_sampleCount * 8.9f * command.MixBufferCount); + } + + public uint Estimate(CopyMixBufferCommand command) + { + // NOTE: Nintendo returns 0 here for some reasons even if it will generate a command like that on version 1.. maybe a mistake? + return 0; + } + + public uint Estimate(MixCommand command) + { + return (uint)(_sampleCount * 10.0f * 1.2f); + } + + public uint Estimate(DelayCommand command) + { + return (uint)(_sampleCount * command.Parameter.ChannelCount * 202.5f); + } + + public uint Estimate(ReverbCommand command) + { + Debug.Assert(command.Parameter.IsChannelCountValid()); + + if (command.Enabled) + { + return (uint)(750 * _sampleCount * command.Parameter.ChannelCount * 1.2f); + } + + return 0; + } + + public uint Estimate(Reverb3dCommand command) + { + if (command.Enabled) + { + return (uint)(530 * _sampleCount * command.Parameter.ChannelCount * 1.2f); + } + + return 0; + } + + public uint Estimate(AuxiliaryBufferCommand command) + { + if (command.Enabled) + { + return 15956; + } + + return 3765; + } + + public uint Estimate(VolumeCommand command) + { + return (uint)(_sampleCount * 8.8f * 1.2f); + } + + public uint Estimate(CircularBufferSinkCommand command) + { + return 55; + } + + public uint Estimate(DownMixSurroundToStereoCommand command) + { + return 16108; + } + + public uint Estimate(UpsampleCommand command) + { + return 357915; + } + + public uint Estimate(DeviceSinkCommand command) + { + return 10042; + } + + public uint Estimate(PcmFloatDataSourceCommandVersion1 command) + { + return 0; + } + + public uint Estimate(DataSourceVersion2Command command) + { + return 0; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion2.cs b/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion2.cs new file mode 100644 index 00000000..0f4fe3c2 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion2.cs @@ -0,0 +1,544 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Dsp.Command; +using System; +using System.Diagnostics; + +namespace Ryujinx.Audio.Renderer.Server +{ + /// <summary> + /// <see cref="ICommandProcessingTimeEstimator"/> version 2. (added with REV5) + /// </summary> + public class CommandProcessingTimeEstimatorVersion2 : ICommandProcessingTimeEstimator + { + private uint _sampleCount; + private uint _bufferCount; + + public CommandProcessingTimeEstimatorVersion2(uint sampleCount, uint bufferCount) + { + _sampleCount = sampleCount; + _bufferCount = bufferCount; + } + + public uint Estimate(PerformanceCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)489.35f; + } + + return (uint)491.18f; + } + + public uint Estimate(ClearMixBufferCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + float costPerBuffer = 668.8f; + float baseCost = 193.2f; + + if (_sampleCount == 160) + { + costPerBuffer = 260.4f; + baseCost = 139.65f; + } + + return (uint)(baseCost + costPerBuffer * _bufferCount); + } + + public uint Estimate(BiquadFilterCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)4813.2f; + } + + return (uint)6915.4f; + } + + public uint Estimate(MixRampGroupedCommand command) + { + const float costPerSample = 7.245f; + + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + int volumeCount = 0; + + for (int i = 0; i < command.MixBufferCount; i++) + { + if (command.Volume0[i] != 0.0f || command.Volume1[i] != 0.0f) + { + volumeCount++; + } + } + + return (uint)(_sampleCount * costPerSample * volumeCount); + } + + public uint Estimate(MixRampCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)1859.0f; + } + + return (uint)2286.1f; + } + + public uint Estimate(DepopPrepareCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)306.62f; + } + + return (uint)293.22f; + } + + public uint Estimate(VolumeRampCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)1403.9f; + } + + return (uint)1884.3f; + } + + public uint Estimate(PcmInt16DataSourceCommandVersion1 command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + float costPerSample = 1195.5f; + float baseCost = 7797.0f; + + if (_sampleCount == 160) + { + costPerSample = 749.27f; + baseCost = 6138.9f; + } + + return (uint)(baseCost + (costPerSample * (((command.SampleRate / 200.0f) / _sampleCount) * (command.Pitch * 0.000030518f)))); + } + + public uint Estimate(AdpcmDataSourceCommandVersion1 command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + float costPerSample = 3564.1f; + float baseCost = 6225.5f; + + if (_sampleCount == 160) + { + costPerSample = 2125.6f; + baseCost = 9039.5f; + } + + return (uint)(baseCost + (costPerSample * (((command.SampleRate / 200.0f) / _sampleCount) * (command.Pitch * 0.000030518f)))); + } + + public uint Estimate(DepopForMixBuffersCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)762.96f; + } + + return (uint)726.96f; + } + + public uint Estimate(CopyMixBufferCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)836.32f; + } + + return (uint)1000.9f; + } + + public uint Estimate(MixCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)1342.2f; + } + + return (uint)1833.2f; + } + + public uint Estimate(DelayCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + if (command.Enabled) + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)41636.0f; + case 2: + return (uint)97861.0f; + case 4: + return (uint)192520.0f; + case 6: + return (uint)301760.0f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + else + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)578.53f; + case 2: + return (uint)663.06f; + case 4: + return (uint)703.98f; + case 6: + return (uint)760.03f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + + } + + if (command.Enabled) + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)8770.3f; + case 2: + return (uint)25741.0f; + case 4: + return (uint)47551.0f; + case 6: + return (uint)81629.0f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + else + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)521.28f; + case 2: + return (uint)585.4f; + case 4: + return (uint)629.88f; + case 6: + return (uint)713.57f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + } + + public uint Estimate(ReverbCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + if (command.Enabled) + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)97192.0f; + case 2: + return (uint)103280.0f; + case 4: + return (uint)109580.0f; + case 6: + return (uint)115070.0f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + else + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)492.01f; + case 2: + return (uint)554.46f; + case 4: + return (uint)595.86f; + case 6: + return (uint)656.62f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + + } + + if (command.Enabled) + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)136460.0f; + case 2: + return (uint)145750.0f; + case 4: + return (uint)154800.0f; + case 6: + return (uint)161970.0f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + else + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)495.79f; + case 2: + return (uint)527.16f; + case 4: + return (uint)598.75f; + case 6: + return (uint)666.03f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + } + + public uint Estimate(Reverb3dCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + if (command.Enabled) + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)138840.0f; + case 2: + return (uint)135430.0f; + case 4: + return (uint)199180.0f; + case 6: + return (uint)247350.0f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + else + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)718.7f; + case 2: + return (uint)751.3f; + case 4: + return (uint)797.46f; + case 6: + return (uint)867.43f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + } + + if (command.Enabled) + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)199950.0f; + case 2: + return (uint)195200.0f; + case 4: + return (uint)290580.0f; + case 6: + return (uint)363490.0f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + else + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)534.24f; + case 2: + return (uint)570.87f; + case 4: + return (uint)660.93f; + case 6: + return (uint)694.6f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + } + + public uint Estimate(AuxiliaryBufferCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + if (command.Enabled) + { + return (uint)7177.9f; + } + + return (uint)489.16f; + } + + if (command.Enabled) + { + return (uint)9499.8f; + } + + return (uint)485.56f; + } + + public uint Estimate(VolumeCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)1280.3f; + } + + return (uint)1737.8f; + } + + public uint Estimate(CircularBufferSinkCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + float costPerBuffer = 1726.0f; + float baseCost = 1369.7f; + + if (_sampleCount == 160) + { + costPerBuffer = 853.63f; + baseCost = 1284.5f; + } + + return (uint)(baseCost + costPerBuffer * command.InputCount); + } + + public uint Estimate(DownMixSurroundToStereoCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)10009.0f; + } + + return (uint)14577.0f; + } + + public uint Estimate(UpsampleCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)292000.0f; + } + + return (uint)0.0f; + } + + public uint Estimate(DeviceSinkCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + Debug.Assert(command.InputCount == 2 || command.InputCount == 6); + + if (command.InputCount == 2) + { + if (_sampleCount == 160) + { + return (uint)9261.5f; + } + + return (uint)9336.1f; + } + + if (_sampleCount == 160) + { + return (uint)9111.8f; + } + + return (uint)9566.7f; + } + + public uint Estimate(PcmFloatDataSourceCommandVersion1 command) + { + // NOTE: This was added between REV7 and REV8 and for some reasons the estimator v2 was changed... + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + float costPerSample = 3490.9f; + float baseCost = 10091.0f; + + if (_sampleCount == 160) + { + costPerSample = 2310.4f; + baseCost = 7845.3f; + } + + return (uint)(baseCost + (costPerSample * (((command.SampleRate / 200.0f) / _sampleCount) * (command.Pitch * 0.000030518f)))); + } + + public uint Estimate(DataSourceVersion2Command command) + { + return 0; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion3.cs b/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion3.cs new file mode 100644 index 00000000..b48ff8b5 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion3.cs @@ -0,0 +1,636 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Common; +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Dsp.Command; +using System; +using System.Diagnostics; +using static Ryujinx.Audio.Renderer.Parameter.VoiceInParameter; + +namespace Ryujinx.Audio.Renderer.Server +{ + /// <summary> + /// <see cref="ICommandProcessingTimeEstimator"/> version 3. (added with REV8) + /// </summary> + public class CommandProcessingTimeEstimatorVersion3 : ICommandProcessingTimeEstimator + { + private uint _sampleCount; + private uint _bufferCount; + + public CommandProcessingTimeEstimatorVersion3(uint sampleCount, uint bufferCount) + { + _sampleCount = sampleCount; + _bufferCount = bufferCount; + } + + public uint Estimate(PerformanceCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)498.17f; + } + + return (uint)489.42f; + } + + public uint Estimate(ClearMixBufferCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + float costPerBuffer = 440.68f; + float baseCost = 0; + + if (_sampleCount == 160) + { + costPerBuffer = 266.65f; + } + + return (uint)(baseCost + costPerBuffer * _bufferCount); + } + + public uint Estimate(BiquadFilterCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)4173.2f; + } + + return (uint)5585.1f; + } + + public uint Estimate(MixRampGroupedCommand command) + { + float costPerSample = 6.4434f; + + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + costPerSample = 6.708f; + } + + int volumeCount = 0; + + for (int i = 0; i < command.MixBufferCount; i++) + { + if (command.Volume0[i] != 0.0f || command.Volume1[i] != 0.0f) + { + volumeCount++; + } + } + + return (uint)(_sampleCount * costPerSample * volumeCount); + } + + public uint Estimate(MixRampCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)1968.7f; + } + + return (uint)2459.4f; + } + + public uint Estimate(DepopPrepareCommand command) + { + return 0; + } + + public uint Estimate(VolumeRampCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)1425.3f; + } + + return (uint)1700.0f; + } + + public uint Estimate(PcmInt16DataSourceCommandVersion1 command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + float costPerSample = 710.143f; + float baseCost = 7853.286f; + + if (_sampleCount == 160) + { + costPerSample = 427.52f; + baseCost = 6329.442f; + } + + return (uint)(baseCost + (costPerSample * (((command.SampleRate / 200.0f) / _sampleCount) * (command.Pitch * 0.000030518f)))); + } + + public uint Estimate(AdpcmDataSourceCommandVersion1 command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + float costPerSample = 3564.1f; + float baseCost = 9736.702f; + + if (_sampleCount == 160) + { + costPerSample = 2125.6f; + baseCost = 7913.808f; + } + + return (uint)(baseCost + (costPerSample * (((command.SampleRate / 200.0f) / _sampleCount) * (command.Pitch * 0.000030518f)))); + } + + public uint Estimate(DepopForMixBuffersCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)739.64f; + } + + return (uint)910.97f; + } + + public uint Estimate(CopyMixBufferCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)842.59f; + } + + return (uint)986.72f; + } + + public uint Estimate(MixCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)1402.8f; + } + + return (uint)1853.2f; + } + + public uint Estimate(DelayCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + if (command.Enabled) + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)8929.04f; + case 2: + return (uint)25500.75f; + case 4: + return (uint)47759.62f; + case 6: + return (uint)82203.07f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + else + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)1295.20f; + case 2: + return (uint)1213.60f; + case 4: + return (uint)942.03f; + case 6: + return (uint)1001.55f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + } + + if (command.Enabled) + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)11941.05f; + case 2: + return (uint)37197.37f; + case 4: + return (uint)69749.84f; + case 6: + return (uint)120042.40f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + else + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)997.67f; + case 2: + return (uint)977.63f; + case 4: + return (uint)792.30f; + case 6: + return (uint)875.43f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + } + + public uint Estimate(ReverbCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + if (command.Enabled) + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)81475.05f; + case 2: + return (uint)84975.0f; + case 4: + return (uint)91625.15f; + case 6: + return (uint)95332.27f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + else + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)536.30f; + case 2: + return (uint)588.70f; + case 4: + return (uint)643.70f; + case 6: + return (uint)706.0f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + } + + if (command.Enabled) + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)120174.47f; + case 2: + return (uint)25262.22f; + case 4: + return (uint)135751.23f; + case 6: + return (uint)141129.23f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + else + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)617.64f; + case 2: + return (uint)659.54f; + case 4: + return (uint)711.43f; + case 6: + return (uint)778.07f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + } + + public uint Estimate(Reverb3dCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + if (command.Enabled) + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)116754.0f; + case 2: + return (uint)125912.05f; + case 4: + return (uint)146336.03f; + case 6: + return (uint)165812.66f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + else + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)734.0f; + case 2: + return (uint)766.62f; + case 4: + return (uint)797.46f; + case 6: + return (uint)867.43f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + } + + if (command.Enabled) + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)170292.34f; + case 2: + return (uint)183875.63f; + case 4: + return (uint)214696.19f; + case 6: + return (uint)243846.77f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + else + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)508.47f; + case 2: + return (uint)582.45f; + case 4: + return (uint)626.42f; + case 6: + return (uint)682.47f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + } + + public uint Estimate(AuxiliaryBufferCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + if (command.Enabled) + { + return (uint)7182.14f; + } + + return (uint)472.11f; + } + + if (command.Enabled) + { + return (uint)9435.96f; + } + + return (uint)462.62f; + } + + public uint Estimate(VolumeCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)1311.1f; + } + + return (uint)1713.6f; + } + + public uint Estimate(CircularBufferSinkCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + float costPerBuffer = 770.26f; + float baseCost = 0f; + + if (_sampleCount == 160) + { + costPerBuffer = 531.07f; + } + + return (uint)(baseCost + costPerBuffer * command.InputCount); + } + + public uint Estimate(DownMixSurroundToStereoCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)9949.7f; + } + + return (uint)14679.0f; + } + + public uint Estimate(UpsampleCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)312990.0f; + } + + return (uint)0.0f; + } + + public uint Estimate(DeviceSinkCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + Debug.Assert(command.InputCount == 2 || command.InputCount == 6); + + if (command.InputCount == 2) + { + if (_sampleCount == 160) + { + return (uint)8980.0f; + } + + return (uint)9221.9f; + } + + if (_sampleCount == 160) + { + return (uint)9177.9f; + } + + return (uint)9725.9f; + } + + public uint Estimate(PcmFloatDataSourceCommandVersion1 command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + float costPerSample = 3490.9f; + float baseCost = 10090.9f; + + if (_sampleCount == 160) + { + costPerSample = 2310.4f; + baseCost = 7845.25f; + } + + return (uint)(baseCost + (costPerSample * (((command.SampleRate / 200.0f) / _sampleCount) * (command.Pitch * 0.000030518f)))); + } + + public uint Estimate(DataSourceVersion2Command command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + (float baseCost, float costPerSample) = GetCostByFormat(_sampleCount, command.SampleFormat, command.SrcQuality); + + return (uint)(baseCost + (costPerSample * (((command.SampleRate / 200.0f) / _sampleCount) * (command.Pitch * 0.000030518f) - 1.0f))); + } + + private static (float, float) GetCostByFormat(uint sampleCount, SampleFormat format, SampleRateConversionQuality quality) + { + Debug.Assert(sampleCount == 160 || sampleCount == 240); + + switch (format) + { + case SampleFormat.PcmInt16: + switch (quality) + { + case SampleRateConversionQuality.Default: + if (sampleCount == 160) + { + return (6329.44f, 427.52f); + } + + return (7853.28f, 710.14f); + case SampleRateConversionQuality.High: + if (sampleCount == 160) + { + return (8049.42f, 371.88f); + } + + return (10138.84f, 610.49f); + case SampleRateConversionQuality.Low: + if (sampleCount == 160) + { + return (5062.66f, 423.43f); + } + + return (5810.96f, 676.72f); + default: + throw new NotImplementedException($"{format} {quality}"); + } + case SampleFormat.PcmFloat: + switch (quality) + { + case SampleRateConversionQuality.Default: + if (sampleCount == 160) + { + return (7845.25f, 2310.4f); + } + + return (10090.9f, 3490.9f); + case SampleRateConversionQuality.High: + if (sampleCount == 160) + { + return (9446.36f, 2308.91f); + } + + return (12520.85f, 3480.61f); + case SampleRateConversionQuality.Low: + if (sampleCount == 160) + { + return (9446.36f, 2308.91f); + } + + return (12520.85f, 3480.61f); + default: + throw new NotImplementedException($"{format} {quality}"); + } + case SampleFormat.Adpcm: + switch (quality) + { + case SampleRateConversionQuality.Default: + if (sampleCount == 160) + { + return (7913.81f, 1827.66f); + } + + return (9736.70f, 2756.37f); + case SampleRateConversionQuality.High: + if (sampleCount == 160) + { + return (9607.81f, 1829.29f); + } + + return (12154.38f, 2731.31f); + case SampleRateConversionQuality.Low: + if (sampleCount == 160) + { + return (6517.48f, 1824.61f); + } + + return (7929.44f, 2732.15f); + default: + throw new NotImplementedException($"{format} {quality}"); + } + default: + throw new NotImplementedException($"{format}"); + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Effect/AuxiliaryBufferEffect.cs b/Ryujinx.Audio/Renderer/Server/Effect/AuxiliaryBufferEffect.cs new file mode 100644 index 00000000..df82945b --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Effect/AuxiliaryBufferEffect.cs @@ -0,0 +1,92 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Dsp.State; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Parameter.Effect; +using Ryujinx.Audio.Renderer.Server.MemoryPool; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using DspAddress = System.UInt64; + +namespace Ryujinx.Audio.Renderer.Server.Effect +{ + /// <summary> + /// Server state for an auxiliary buffer effect. + /// </summary> + public class AuxiliaryBufferEffect : BaseEffect + { + /// <summary> + /// The auxiliary buffer parameter. + /// </summary> + public AuxiliaryBufferParameter Parameter; + + /// <summary> + /// Auxiliary buffer state. + /// </summary> + public AuxiliaryBufferAddresses State; + + public override EffectType TargetEffectType => EffectType.AuxiliaryBuffer; + + public override DspAddress GetWorkBuffer(int index) + { + return WorkBuffers[index].GetReference(true); + } + + public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameter parameter, PoolMapper mapper) + { + Debug.Assert(IsTypeValid(ref parameter)); + + UpdateParameterBase(ref parameter); + + Parameter = MemoryMarshal.Cast<byte, AuxiliaryBufferParameter>(parameter.SpecificData)[0]; + IsEnabled = parameter.IsEnabled; + + updateErrorInfo = new BehaviourParameter.ErrorInfo(); + + if (BufferUnmapped || parameter.IsNew) + { + ulong bufferSize = (ulong)Unsafe.SizeOf<int>() * Parameter.BufferStorageSize + (ulong)Unsafe.SizeOf<AuxiliaryBufferHeader>() * 2; + + bool sendBufferUnmapped = !mapper.TryAttachBuffer(out updateErrorInfo, ref WorkBuffers[0], Parameter.SendBufferInfoAddress, bufferSize); + bool returnBufferUnmapped = !mapper.TryAttachBuffer(out updateErrorInfo, ref WorkBuffers[1], Parameter.ReturnBufferInfoAddress, bufferSize); + + BufferUnmapped = sendBufferUnmapped && returnBufferUnmapped; + + if (!BufferUnmapped) + { + DspAddress sendDspAddress = WorkBuffers[0].GetReference(false); + DspAddress returnDspAddress = WorkBuffers[1].GetReference(false); + + State.SendBufferInfo = sendDspAddress + (uint)Unsafe.SizeOf<AuxiliaryBufferHeader>(); + State.SendBufferInfoBase = sendDspAddress + (uint)Unsafe.SizeOf<AuxiliaryBufferHeader>() * 2; + + State.ReturnBufferInfo = returnDspAddress + (uint)Unsafe.SizeOf<AuxiliaryBufferHeader>(); + State.ReturnBufferInfoBase = returnDspAddress + (uint)Unsafe.SizeOf<AuxiliaryBufferHeader>() * 2; + } + } + } + + public override void UpdateForCommandGeneration() + { + UpdateUsageStateForCommandGeneration(); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Effect/BaseEffect.cs b/Ryujinx.Audio/Renderer/Server/Effect/BaseEffect.cs new file mode 100644 index 00000000..7529f894 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Effect/BaseEffect.cs @@ -0,0 +1,257 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Server.MemoryPool; +using Ryujinx.Audio.Renderer.Utils; +using System; +using System.Diagnostics; +using static Ryujinx.Audio.Renderer.Common.BehaviourParameter; + +using DspAddress = System.UInt64; + +namespace Ryujinx.Audio.Renderer.Server.Effect +{ + /// <summary> + /// Base class used as a server state for an effect. + /// </summary> + public class BaseEffect + { + /// <summary> + /// The <see cref="EffectType"/> of the effect. + /// </summary> + public EffectType Type; + + /// <summary> + /// Set to true if the effect must be active. + /// </summary> + public bool IsEnabled; + + /// <summary> + /// Set to true if the internal effect work buffers used wasn't mapped. + /// </summary> + public bool BufferUnmapped; + + /// <summary> + /// The current state of the effect. + /// </summary> + public UsageState UsageState; + + /// <summary> + /// The target mix id of the effect. + /// </summary> + public int MixId; + + /// <summary> + /// Position of the effect while processing effects. + /// </summary> + public uint ProcessingOrder; + + /// <summary> + /// Array of all the work buffer used by the effect. + /// </summary> + protected AddressInfo[] WorkBuffers; + + /// <summary> + /// Create a new <see cref="BaseEffect"/>. + /// </summary> + public BaseEffect() + { + Type = TargetEffectType; + UsageState = UsageState.Invalid; + + IsEnabled = false; + BufferUnmapped = false; + MixId = Constants.UnusedMixId; + ProcessingOrder = uint.MaxValue; + + WorkBuffers = new AddressInfo[2]; + + foreach (ref AddressInfo info in WorkBuffers.AsSpan()) + { + info = AddressInfo.Create(); + } + } + + /// <summary> + /// The target <see cref="EffectType"/> handled by this <see cref="BaseEffect"/>. + /// </summary> + public virtual EffectType TargetEffectType => EffectType.Invalid; + + /// <summary> + /// Check if the <see cref="EffectType"/> sent by the user match the internal <see cref="EffectType"/>. + /// </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(ref EffectInParameter parameter) + { + return parameter.Type == TargetEffectType; + } + + /// <summary> + /// Update the usage state during command generation. + /// </summary> + protected void UpdateUsageStateForCommandGeneration() + { + UsageState = IsEnabled ? UsageState.Enabled : UsageState.Disabled; + } + + /// <summary> + /// Update the internal common parameters from a user parameter. + /// </summary> + /// <param name="parameter">The user parameter.</param> + protected void UpdateParameterBase(ref EffectInParameter parameter) + { + MixId = parameter.MixId; + ProcessingOrder = parameter.ProcessingOrder; + } + + /// <summary> + /// Force unmap all the work buffers. + /// </summary> + /// <param name="mapper">The mapper to use.</param> + public void ForceUnmapBuffers(PoolMapper mapper) + { + foreach (ref AddressInfo info in WorkBuffers.AsSpan()) + { + if (info.GetReference(false) != 0) + { + mapper.ForceUnmap(ref info); + } + } + } + + /// <summary> + /// Check if the effect needs to be skipped. + /// </summary> + /// <returns>Returns true if the effect needs to be skipped.</returns> + public bool ShouldSkip() + { + return BufferUnmapped; + } + + /// <summary> + /// Update the <see cref="BaseEffect"/> state during command generation. + /// </summary> + public virtual void UpdateForCommandGeneration() + { + Debug.Assert(Type == TargetEffectType); + } + + /// <summary> + /// Update the internal state from a user parameter. + /// </summary> + /// <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 EffectInParameter parameter, PoolMapper mapper) + { + Debug.Assert(IsTypeValid(ref parameter)); + + updateErrorInfo = new ErrorInfo(); + } + + /// <summary> + /// Get the work buffer DSP address at the given index. + /// </summary> + /// <param name="index">The index of the work buffer</param> + /// <returns>The work buffer DSP address at the given index.</returns> + public virtual DspAddress GetWorkBuffer(int index) + { + throw new InvalidOperationException(); + } + + /// <summary> + /// Get the first work buffer DSP address. + /// </summary> + /// <returns>The first work buffer DSP address.</returns> + protected DspAddress GetSingleBuffer() + { + if (IsEnabled) + { + return WorkBuffers[0].GetReference(true); + } + + if (UsageState != UsageState.Disabled) + { + DspAddress address = WorkBuffers[0].GetReference(false); + ulong size = WorkBuffers[0].Size; + + if (address != 0 && size != 0) + { + AudioProcessorMemoryManager.InvalidateDataCache(address, size); + } + } + + return 0; + } + + /// <summary> + /// Store the output status to the given user output. + /// </summary> + /// <param name="outStatus">The given user output.</param> + /// <param name="isAudioRendererActive">If set to true, the <see cref="AudioRenderSystem"/> is active.</param> + public void StoreStatus(ref EffectOutStatus outStatus, bool isAudioRendererActive) + { + if (isAudioRendererActive) + { + if (UsageState == UsageState.Disabled) + { + outStatus.State = EffectOutStatus.EffectState.Disabled; + } + else + { + outStatus.State = EffectOutStatus.EffectState.Enabled; + } + } + else if (UsageState == UsageState.New) + { + outStatus.State = EffectOutStatus.EffectState.Enabled; + } + else + { + outStatus.State = EffectOutStatus.EffectState.Disabled; + } + } + + /// <summary> + /// Get the <see cref="PerformanceDetailType"/> associated to the <see cref="Type"/> of this effect. + /// </summary> + /// <returns>The <see cref="PerformanceDetailType"/> associated to the <see cref="Type"/> of this effect.</returns> + public PerformanceDetailType GetPerformanceDetailType() + { + switch (Type) + { + case EffectType.BiquadFilter: + return PerformanceDetailType.BiquadFilter; + case EffectType.AuxiliaryBuffer: + return PerformanceDetailType.Aux; + case EffectType.Delay: + return PerformanceDetailType.Delay; + case EffectType.Reverb: + return PerformanceDetailType.Reverb; + case EffectType.Reverb3d: + return PerformanceDetailType.Reverb3d; + case EffectType.BufferMix: + return PerformanceDetailType.Mix; + default: + throw new NotImplementedException($"{Type}"); + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Effect/BiquadFilterEffect.cs b/Ryujinx.Audio/Renderer/Server/Effect/BiquadFilterEffect.cs new file mode 100644 index 00000000..35ba8a0d --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Effect/BiquadFilterEffect.cs @@ -0,0 +1,74 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Dsp.State; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Parameter.Effect; +using Ryujinx.Audio.Renderer.Server.MemoryPool; +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Effect +{ + /// <summary> + /// Server state for a biquad filter effect. + /// </summary> + public class BiquadFilterEffect : BaseEffect + { + /// <summary> + /// The biquad filter parameter. + /// </summary> + public BiquadFilterEffectParameter Parameter; + + /// <summary> + /// The biquad filter state. + /// </summary> + public Memory<BiquadFilterState> State { get; } + + /// <summary> + /// Create a new <see cref="BiquadFilterEffect"/>. + /// </summary> + public BiquadFilterEffect() + { + Parameter = new BiquadFilterEffectParameter(); + State = new BiquadFilterState[Constants.ChannelCountMax]; + } + + public override EffectType TargetEffectType => EffectType.BiquadFilter; + + public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameter parameter, PoolMapper mapper) + { + Debug.Assert(IsTypeValid(ref parameter)); + + UpdateParameterBase(ref parameter); + + Parameter = MemoryMarshal.Cast<byte, BiquadFilterEffectParameter>(parameter.SpecificData)[0]; + IsEnabled = parameter.IsEnabled; + + updateErrorInfo = new BehaviourParameter.ErrorInfo(); + } + + public override void UpdateForCommandGeneration() + { + UpdateUsageStateForCommandGeneration(); + + Parameter.Status = UsageState.Enabled; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Effect/BufferMixEffect.cs b/Ryujinx.Audio/Renderer/Server/Effect/BufferMixEffect.cs new file mode 100644 index 00000000..14ed3950 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Effect/BufferMixEffect.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Parameter.Effect; +using Ryujinx.Audio.Renderer.Server.MemoryPool; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Effect +{ + /// <summary> + /// Server state for a buffer mix effect. + /// </summary> + public class BufferMixEffect : BaseEffect + { + /// <summary> + /// The buffer mix parameter. + /// </summary> + public BufferMixParameter Parameter; + + public override EffectType TargetEffectType => EffectType.BufferMix; + + public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameter parameter, PoolMapper mapper) + { + Debug.Assert(IsTypeValid(ref parameter)); + + UpdateParameterBase(ref parameter); + + Parameter = MemoryMarshal.Cast<byte, BufferMixParameter>(parameter.SpecificData)[0]; + IsEnabled = parameter.IsEnabled; + + updateErrorInfo = new BehaviourParameter.ErrorInfo(); + } + + public override void UpdateForCommandGeneration() + { + UpdateUsageStateForCommandGeneration(); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Effect/DelayEffect.cs b/Ryujinx.Audio/Renderer/Server/Effect/DelayEffect.cs new file mode 100644 index 00000000..df3e5ee7 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Effect/DelayEffect.cs @@ -0,0 +1,100 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Dsp.State; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Parameter.Effect; +using Ryujinx.Audio.Renderer.Server.MemoryPool; +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using DspAddress = System.UInt64; + +namespace Ryujinx.Audio.Renderer.Server.Effect +{ + /// <summary> + /// Server state for a delay effect. + /// </summary> + public class DelayEffect : BaseEffect + { + /// <summary> + /// The delay parameter. + /// </summary> + public DelayParameter Parameter; + + /// <summary> + /// The delay state. + /// </summary> + public Memory<DelayState> State { get; } + + public DelayEffect() + { + State = new DelayState[1]; + } + + public override EffectType TargetEffectType => EffectType.Delay; + + public override DspAddress GetWorkBuffer(int index) + { + return GetSingleBuffer(); + } + + public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameter parameter, PoolMapper mapper) + { + Debug.Assert(IsTypeValid(ref parameter)); + + ref DelayParameter delayParameter = ref MemoryMarshal.Cast<byte, DelayParameter>(parameter.SpecificData)[0]; + + updateErrorInfo = new BehaviourParameter.ErrorInfo(); + + if (delayParameter.IsChannelCountMaxValid()) + { + UpdateParameterBase(ref parameter); + + UsageState oldParameterStatus = Parameter.Status; + + Parameter = delayParameter; + + if (delayParameter.IsChannelCountValid()) + { + IsEnabled = parameter.IsEnabled; + + if (oldParameterStatus != UsageState.Enabled) + { + Parameter.Status = oldParameterStatus; + } + + if (BufferUnmapped || parameter.IsNew) + { + UsageState = UsageState.New; + Parameter.Status = UsageState.Invalid; + + BufferUnmapped = !mapper.TryAttachBuffer(out updateErrorInfo, ref WorkBuffers[0], parameter.BufferBase, parameter.BufferSize); + } + } + } + } + + public override void UpdateForCommandGeneration() + { + UpdateUsageStateForCommandGeneration(); + + Parameter.Status = UsageState.Enabled; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Effect/EffectContext.cs b/Ryujinx.Audio/Renderer/Server/Effect/EffectContext.cs new file mode 100644 index 00000000..ff6051ae --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Effect/EffectContext.cs @@ -0,0 +1,82 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using System.Diagnostics; + +namespace Ryujinx.Audio.Renderer.Server.Effect +{ + /// <summary> + /// Effect context. + /// </summary> + public class EffectContext + { + /// <summary> + /// Storage for <see cref="BaseEffect"/>. + /// </summary> + private BaseEffect[] _effects; + + /// <summary> + /// The total effect count. + /// </summary> + private uint _effectCount; + + /// <summary> + /// Create a new <see cref="EffectContext"/>. + /// </summary> + public EffectContext() + { + _effects = null; + _effectCount = 0; + } + + /// <summary> + /// Initialize the <see cref="EffectContext"/>. + /// </summary> + /// <param name="effectCount">The total effect count.</param> + public void Initialize(uint effectCount) + { + _effectCount = effectCount; + _effects = new BaseEffect[effectCount]; + + for (int i = 0; i < _effectCount; i++) + { + _effects[i] = new BaseEffect(); + } + } + + /// <summary> + /// Get the total effect count. + /// </summary> + /// <returns>The total effect count.</returns> + public uint GetCount() + { + return _effectCount; + } + + /// <summary> + /// Get a reference to a <see cref="BaseEffect"/> at the given <paramref name="index"/>. + /// </summary> + /// <param name="index">The index to use.</param> + /// <returns>A reference to a <see cref="BaseEffect"/> at the given <paramref name="index"/>.</returns> + public ref BaseEffect GetEffect(int index) + { + Debug.Assert(index >= 0 && index < _effectCount); + + return ref _effects[index]; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Effect/Reverb3dEffect.cs b/Ryujinx.Audio/Renderer/Server/Effect/Reverb3dEffect.cs new file mode 100644 index 00000000..d9f23799 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Effect/Reverb3dEffect.cs @@ -0,0 +1,99 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Dsp.State; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Parameter.Effect; +using Ryujinx.Audio.Renderer.Server.MemoryPool; +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Effect +{ + /// <summary> + /// Server state for a 3D reverberation effect. + /// </summary> + public class Reverb3dEffect : BaseEffect + { + /// <summary> + /// The 3D reverberation parameter. + /// </summary> + public Reverb3dParameter Parameter; + + /// <summary> + /// The 3D reverberation state. + /// </summary> + public Memory<Reverb3dState> State { get; } + + public Reverb3dEffect() + { + State = new Reverb3dState[1]; + } + + public override EffectType TargetEffectType => EffectType.Reverb3d; + + public override ulong GetWorkBuffer(int index) + { + return GetSingleBuffer(); + } + + public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameter parameter, PoolMapper mapper) + { + Debug.Assert(IsTypeValid(ref parameter)); + + ref Reverb3dParameter reverbParameter = ref MemoryMarshal.Cast<byte, Reverb3dParameter>(parameter.SpecificData)[0]; + + updateErrorInfo = new BehaviourParameter.ErrorInfo(); + + if (reverbParameter.IsChannelCountMaxValid()) + { + UpdateParameterBase(ref parameter); + + UsageState oldParameterStatus = Parameter.ParameterStatus; + + Parameter = reverbParameter; + + if (reverbParameter.IsChannelCountValid()) + { + IsEnabled = parameter.IsEnabled; + + if (oldParameterStatus != UsageState.Enabled) + { + Parameter.ParameterStatus = oldParameterStatus; + } + + if (BufferUnmapped || parameter.IsNew) + { + UsageState = UsageState.New; + Parameter.ParameterStatus = UsageState.Invalid; + + BufferUnmapped = !mapper.TryAttachBuffer(out updateErrorInfo, ref WorkBuffers[0], parameter.BufferBase, parameter.BufferSize); + } + } + } + } + + public override void UpdateForCommandGeneration() + { + UpdateUsageStateForCommandGeneration(); + + Parameter.ParameterStatus = UsageState.Enabled; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Effect/ReverbEffect.cs b/Ryujinx.Audio/Renderer/Server/Effect/ReverbEffect.cs new file mode 100644 index 00000000..4c81f729 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Effect/ReverbEffect.cs @@ -0,0 +1,102 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Dsp.State; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Parameter.Effect; +using Ryujinx.Audio.Renderer.Server.MemoryPool; +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Effect +{ + /// <summary> + /// Server state for a reverberation effect. + /// </summary> + public class ReverbEffect : BaseEffect + { + /// <summary> + /// The reverberation parameter. + /// </summary> + public ReverbParameter Parameter; + + /// <summary> + /// The reverberation state. + /// </summary> + public Memory<ReverbState> State { get; } + + /// <summary> + /// Create a new <see cref="ReverbEffect"/>. + /// </summary> + public ReverbEffect() + { + State = new ReverbState[1]; + } + + public override EffectType TargetEffectType => EffectType.Reverb; + + public override ulong GetWorkBuffer(int index) + { + return GetSingleBuffer(); + } + + public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameter parameter, PoolMapper mapper) + { + Debug.Assert(IsTypeValid(ref parameter)); + + ref ReverbParameter reverbParameter = ref MemoryMarshal.Cast<byte, ReverbParameter>(parameter.SpecificData)[0]; + + updateErrorInfo = new BehaviourParameter.ErrorInfo(); + + if (reverbParameter.IsChannelCountMaxValid()) + { + UpdateParameterBase(ref parameter); + + UsageState oldParameterStatus = Parameter.Status; + + Parameter = reverbParameter; + + if (reverbParameter.IsChannelCountValid()) + { + IsEnabled = parameter.IsEnabled; + + if (oldParameterStatus != UsageState.Enabled) + { + Parameter.Status = oldParameterStatus; + } + + if (BufferUnmapped || parameter.IsNew) + { + UsageState = UsageState.New; + Parameter.Status = UsageState.Invalid; + + BufferUnmapped = !mapper.TryAttachBuffer(out updateErrorInfo, ref WorkBuffers[0], parameter.BufferBase, parameter.BufferSize); + } + } + } + } + + public override void UpdateForCommandGeneration() + { + UpdateUsageStateForCommandGeneration(); + + Parameter.Status = UsageState.Enabled; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Effect/UsageState.cs b/Ryujinx.Audio/Renderer/Server/Effect/UsageState.cs new file mode 100644 index 00000000..a519acd4 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Effect/UsageState.cs @@ -0,0 +1,45 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +namespace Ryujinx.Audio.Renderer.Server.Effect +{ + /// <summary> + /// The usage state of an effect. + /// </summary> + public enum UsageState : byte + { + /// <summary> + /// The effect is in an invalid state. + /// </summary> + Invalid, + + /// <summary> + /// The effect is new. + /// </summary> + New, + + /// <summary> + /// The effect is enabled. + /// </summary> + Enabled, + + /// <summary> + /// The effect is disabled. + /// </summary> + Disabled + } +} diff --git a/Ryujinx.Audio/Renderer/Server/ICommandProcessingTimeEstimator.cs b/Ryujinx.Audio/Renderer/Server/ICommandProcessingTimeEstimator.cs new file mode 100644 index 00000000..119ec907 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/ICommandProcessingTimeEstimator.cs @@ -0,0 +1,52 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Dsp.Command; + +namespace Ryujinx.Audio.Renderer.Server +{ + /// <summary> + /// Estimate the time that a <see cref="ICommand"/> should take. + /// </summary> + /// <remarks>This is used for voice dropping.</remarks> + public interface ICommandProcessingTimeEstimator + { + uint Estimate(AuxiliaryBufferCommand command); + uint Estimate(BiquadFilterCommand command); + uint Estimate(ClearMixBufferCommand command); + uint Estimate(DelayCommand command); + uint Estimate(Reverb3dCommand command); + uint Estimate(ReverbCommand command); + uint Estimate(DepopPrepareCommand command); + uint Estimate(DepopForMixBuffersCommand command); + uint Estimate(MixCommand command); + uint Estimate(MixRampCommand command); + uint Estimate(MixRampGroupedCommand command); + uint Estimate(CopyMixBufferCommand command); + uint Estimate(PerformanceCommand command); + uint Estimate(VolumeCommand command); + uint Estimate(VolumeRampCommand command); + uint Estimate(PcmInt16DataSourceCommandVersion1 command); + uint Estimate(PcmFloatDataSourceCommandVersion1 command); + uint Estimate(AdpcmDataSourceCommandVersion1 command); + uint Estimate(DataSourceVersion2Command command); + uint Estimate(CircularBufferSinkCommand command); + uint Estimate(DeviceSinkCommand command); + uint Estimate(DownMixSurroundToStereoCommand command); + uint Estimate(UpsampleCommand command); + } +} diff --git a/Ryujinx.Audio/Renderer/Server/MemoryPool/AddressInfo.cs b/Ryujinx.Audio/Renderer/Server/MemoryPool/AddressInfo.cs new file mode 100644 index 00000000..2b625d06 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/MemoryPool/AddressInfo.cs @@ -0,0 +1,151 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using System; +using System.Runtime.InteropServices; + +using DspAddress = System.UInt64; +using CpuAddress = System.UInt64; + +namespace Ryujinx.Audio.Renderer.Server.MemoryPool +{ + /// <summary> + /// Represents the information of a region shared between the CPU and DSP. + /// </summary> + public struct AddressInfo + { + /// <summary> + /// The target CPU address of the region. + /// </summary> + public CpuAddress CpuAddress; + + /// <summary> + /// The size of the region. + /// </summary> + public ulong Size; + + private unsafe MemoryPoolState* _memoryPools; + + /// <summary> + /// The forced DSP address of the region. + /// </summary> + public DspAddress ForceMappedDspAddress; + + private unsafe ref MemoryPoolState MemoryPoolState => ref *_memoryPools; + + public unsafe bool HasMemoryPoolState => (IntPtr)_memoryPools != IntPtr.Zero; + + /// <summary> + /// Create an new empty <see cref="AddressInfo"/>. + /// </summary> + /// <returns>A new empty <see cref="AddressInfo"/>.</returns> + public static AddressInfo Create() + { + return Create(0, 0); + } + + /// <summary> + /// Create a new <see cref="AddressInfo"/>. + /// </summary> + /// <param name="cpuAddress">The target <see cref="CpuAddress"/> of the region.</param> + /// <param name="size">The target size of the region.</param> + /// <returns>A new <see cref="AddressInfo"/>.</returns> + public static AddressInfo Create(CpuAddress cpuAddress, ulong size) + { + unsafe + { + return new AddressInfo + { + CpuAddress = cpuAddress, + _memoryPools = MemoryPoolState.Null, + Size = size, + ForceMappedDspAddress = 0 + }; + } + } + + /// <summary> + /// Setup the CPU address and size of the <see cref="AddressInfo"/>. + /// </summary> + /// <param name="cpuAddress">The target <see cref="CpuAddress"/> of the region.</param> + /// <param name="size">The size of the region.</param> + public void Setup(CpuAddress cpuAddress, ulong size) + { + CpuAddress = cpuAddress; + Size = size; + ForceMappedDspAddress = 0; + + unsafe + { + _memoryPools = MemoryPoolState.Null; + } + } + + /// <summary> + /// Set the <see cref="MemoryPoolState"/> associated. + /// </summary> + /// <param name="memoryPoolState">The <see cref="MemoryPoolState"/> associated.</param> + public void SetupMemoryPool(Span<MemoryPoolState> memoryPoolState) + { + unsafe + { + fixed (MemoryPoolState* ptr = &MemoryMarshal.GetReference(memoryPoolState)) + { + SetupMemoryPool(ptr); + } + } + } + + /// <summary> + /// Set the <see cref="MemoryPoolState"/> associated. + /// </summary> + /// <param name="memoryPoolState">The <see cref="MemoryPoolState"/> associated.</param> + public unsafe void SetupMemoryPool(MemoryPoolState* memoryPoolState) + { + _memoryPools = memoryPoolState; + } + + /// <summary> + /// Check if the <see cref="MemoryPoolState"/> is mapped. + /// </summary> + /// <returns>Returns true if the <see cref="MemoryPoolState"/> is mapped.</returns> + public bool HasMappedMemoryPool() + { + return HasMemoryPoolState && MemoryPoolState.IsMapped(); + } + + /// <summary> + /// Get the DSP address associated to the <see cref="AddressInfo"/>. + /// </summary> + /// <param name="markUsed">If true, mark the <see cref="MemoryPoolState"/> as used.</param> + /// <returns>Returns the DSP address associated to the <see cref="AddressInfo"/>.</returns> + public DspAddress GetReference(bool markUsed) + { + if (!HasMappedMemoryPool()) + { + return ForceMappedDspAddress; + } + + if (markUsed) + { + MemoryPoolState.IsUsed = true; + } + + return MemoryPoolState.Translate(CpuAddress, Size); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/MemoryPool/MemoryPoolState.cs b/Ryujinx.Audio/Renderer/Server/MemoryPool/MemoryPoolState.cs new file mode 100644 index 00000000..ebde1403 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/MemoryPool/MemoryPoolState.cs @@ -0,0 +1,148 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using System; +using System.Runtime.InteropServices; + +using DspAddress = System.UInt64; +using CpuAddress = System.UInt64; + +namespace Ryujinx.Audio.Renderer.Server.MemoryPool +{ + /// <summary> + /// Server state for a memory pool. + /// </summary> + [StructLayout(LayoutKind.Sequential, Size = 0x20, Pack = Alignment)] + public struct MemoryPoolState + { + public const int Alignment = 0x10; + + /// <summary> + /// The location of the <see cref="MemoryPoolState"/>. + /// </summary> + public enum LocationType : uint + { + /// <summary> + /// <see cref="MemoryPoolState"/> located on the CPU side for user use. + /// </summary> + Cpu, + + /// <summary> + /// <see cref="MemoryPoolState"/> located on the DSP side for system use. + /// </summary> + Dsp + } + + /// <summary> + /// The CPU address associated to the <see cref="MemoryPoolState"/>. + /// </summary> + public CpuAddress CpuAddress; + + /// <summary> + /// The DSP address associated to the <see cref="MemoryPoolState"/>. + /// </summary> + public DspAddress DspAddress; + + /// <summary> + /// The size associated to the <see cref="MemoryPoolState"/>. + /// </summary> + public ulong Size; + + /// <summary> + /// The <see cref="LocationType"/> associated to the <see cref="MemoryPoolState"/>. + /// </summary> + public LocationType Location; + + /// <summary> + /// Set to true if the <see cref="MemoryPoolState"/> is used. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool IsUsed; + + public static unsafe MemoryPoolState* Null => (MemoryPoolState*)IntPtr.Zero.ToPointer(); + + /// <summary> + /// Create a new <see cref="MemoryPoolState"/> with the given <see cref="LocationType"/>. + /// </summary> + /// <param name="location">The location type to use.</param> + /// <returns>A new <see cref="MemoryPoolState"/> with the given <see cref="LocationType"/>.</returns> + public static MemoryPoolState Create(LocationType location) + { + return new MemoryPoolState + { + CpuAddress = 0, + DspAddress = 0, + Size = 0, + Location = location + }; + } + + /// <summary> + /// Set the <see cref="CpuAddress"/> and size of the <see cref="MemoryPoolState"/>. + /// </summary> + /// <param name="cpuAddress">The <see cref="CpuAddress"/>.</param> + /// <param name="size">The size.</param> + public void SetCpuAddress(CpuAddress cpuAddress, ulong size) + { + CpuAddress = cpuAddress; + Size = size; + } + + /// <summary> + /// Check if the given <see cref="CpuAddress"/> and size is contains in the <see cref="MemoryPoolState"/>. + /// </summary> + /// <param name="targetCpuAddress">The <see cref="CpuAddress"/>.</param> + /// <param name="size">The size.</param> + /// <returns>True if the <see cref="CpuAddress"/> is contained inside the <see cref="MemoryPoolState"/>.</returns> + public bool Contains(CpuAddress targetCpuAddress, ulong size) + { + if (CpuAddress <= targetCpuAddress && size + targetCpuAddress <= Size + CpuAddress) + { + return true; + } + + return false; + } + + /// <summary> + /// Translate the given CPU address to a DSP address. + /// </summary> + /// <param name="targetCpuAddress">The <see cref="CpuAddress"/>.</param> + /// <param name="size">The size.</param> + /// <returns>the target DSP address.</returns> + public DspAddress Translate(CpuAddress targetCpuAddress, ulong size) + { + if (Contains(targetCpuAddress, size) && IsMapped()) + { + ulong offset = targetCpuAddress - CpuAddress; + + return DspAddress + offset; + } + + return 0; + } + + /// <summary> + /// Is the <see cref="MemoryPoolState"/> mapped on the DSP? + /// </summary> + /// <returns>Returns true if the <see cref="MemoryPoolState"/> is mapped on the DSP.</returns> + public bool IsMapped() + { + return DspAddress != 0; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/MemoryPool/PoolMapper.cs b/Ryujinx.Audio/Renderer/Server/MemoryPool/PoolMapper.cs new file mode 100644 index 00000000..b255960c --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/MemoryPool/PoolMapper.cs @@ -0,0 +1,383 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Utils; +using Ryujinx.Common.Logging; +using System; +using static Ryujinx.Audio.Renderer.Common.BehaviourParameter; + +using CpuAddress = System.UInt64; +using DspAddress = System.UInt64; + +namespace Ryujinx.Audio.Renderer.Server.MemoryPool +{ + /// <summary> + /// Memory pool mapping helper. + /// </summary> + public class PoolMapper + { + const uint CurrentProcessPseudoHandle = 0xFFFF8001; + + /// <summary> + /// The result of <see cref="Update(ref MemoryPoolState, ref MemoryPoolInParameter, ref MemoryPoolOutStatus)"/>. + /// </summary> + public enum UpdateResult : uint + { + /// <summary> + /// No error reported. + /// </summary> + Success = 0, + + /// <summary> + /// The user parameters were invalid. + /// </summary> + InvalidParameter = 1, + + /// <summary> + /// <see cref="Dsp.AudioProcessor"/> mapping failed. + /// </summary> + MapError = 2, + + /// <summary> + /// <see cref="Dsp.AudioProcessor"/> unmapping failed. + /// </summary> + UnmapError = 3 + } + + /// <summary> + /// The handle of the process owning the CPU memory manipulated. + /// </summary> + private uint _processHandle; + + /// <summary> + /// The <see cref="Memory{MemoryPoolState}"/> that will be manipulated. + /// </summary> + private Memory<MemoryPoolState> _memoryPools; + + /// <summary> + /// If set to true, this will try to force map memory pool even if their state are considered invalid. + /// </summary> + private bool _isForceMapEnabled; + + /// <summary> + /// Create a new <see cref="PoolMapper"/> used for system mapping. + /// </summary> + /// <param name="processHandle">The handle of the process owning the CPU memory manipulated.</param> + /// <param name="isForceMapEnabled">If set to true, this will try to force map memory pool even if their state are considered invalid.</param> + public PoolMapper(uint processHandle, bool isForceMapEnabled) + { + _processHandle = processHandle; + _isForceMapEnabled = isForceMapEnabled; + _memoryPools = Memory<MemoryPoolState>.Empty; + } + + /// <summary> + /// Create a new <see cref="PoolMapper"/> used for user mapping. + /// </summary> + /// <param name="processHandle">The handle of the process owning the CPU memory manipulated.</param> + /// <param name="memoryPool">The user memory pools.</param> + /// <param name="isForceMapEnabled">If set to true, this will try to force map memory pool even if their state are considered invalid.</param> + public PoolMapper(uint processHandle, Memory<MemoryPoolState> memoryPool, bool isForceMapEnabled) + { + _processHandle = processHandle; + _memoryPools = memoryPool; + _isForceMapEnabled = isForceMapEnabled; + } + + /// <summary> + /// Initialize the <see cref="MemoryPoolState"/> for system use. + /// </summary> + /// <param name="memoryPool">The <see cref="MemoryPoolState"/> for system use.</param> + /// <param name="cpuAddress">The <see cref="CpuAddress"/> to assign.</param> + /// <param name="size">The size to assign.</param> + /// <returns>Returns true if mapping on the <see cref="Dsp.AudioProcessor"/> succeeded.</returns> + public bool InitializeSystemPool(ref MemoryPoolState memoryPool, CpuAddress cpuAddress, ulong size) + { + if (memoryPool.Location != MemoryPoolState.LocationType.Dsp) + { + return false; + } + + return InitializePool(ref memoryPool, cpuAddress, size); + } + + /// <summary> + /// Initialize the <see cref="MemoryPoolState"/>. + /// </summary> + /// <param name="memoryPool">The <see cref="MemoryPoolState"/>.</param> + /// <param name="cpuAddress">The <see cref="CpuAddress"/> to assign.</param> + /// <param name="size">The size to assign.</param> + /// <returns>Returns true if mapping on the <see cref="Dsp.AudioProcessor"/> succeeded.</returns> + public bool InitializePool(ref MemoryPoolState memoryPool, CpuAddress cpuAddress, ulong size) + { + memoryPool.SetCpuAddress(cpuAddress, size); + + return Map(ref memoryPool) != 0; + } + + /// <summary> + /// Get the process handle associated to the <see cref="MemoryPoolState"/>. + /// </summary> + /// <param name="memoryPool">The <see cref="MemoryPoolState"/>.</param> + /// <returns>Returns the process handle associated to the <see cref="MemoryPoolState"/>.</returns> + public uint GetProcessHandle(ref MemoryPoolState memoryPool) + { + if (memoryPool.Location == MemoryPoolState.LocationType.Cpu) + { + return CurrentProcessPseudoHandle; + } + else if (memoryPool.Location == MemoryPoolState.LocationType.Dsp) + { + return _processHandle; + } + + return 0; + } + + /// <summary> + /// Map the <see cref="MemoryPoolState"/> on the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + /// <param name="memoryPool">The <see cref="MemoryPoolState"/> to map.</param> + /// <returns>Returns the DSP address mapped.</returns> + public DspAddress Map(ref MemoryPoolState memoryPool) + { + DspAddress result = AudioProcessorMemoryManager.Map(GetProcessHandle(ref memoryPool), memoryPool.CpuAddress, memoryPool.Size); + + if (result != 0) + { + memoryPool.DspAddress = result; + } + + return result; + } + + /// <summary> + /// Unmap the <see cref="MemoryPoolState"/> from the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + /// <param name="memoryPool">The <see cref="MemoryPoolState"/> to unmap.</param> + /// <returns>Returns true if unmapped.</returns> + public bool Unmap(ref MemoryPoolState memoryPool) + { + if (memoryPool.IsUsed) + { + return false; + } + + AudioProcessorMemoryManager.Unmap(GetProcessHandle(ref memoryPool), memoryPool.CpuAddress, memoryPool.Size); + + memoryPool.SetCpuAddress(0, 0); + memoryPool.DspAddress = 0; + + return true; + } + + /// <summary> + /// Find a <see cref="MemoryPoolState"/> associated to the region given. + /// </summary> + /// <param name="cpuAddress">The region <see cref="CpuAddress"/>.</param> + /// <param name="size">The region size.</param> + /// <returns>Returns the <see cref="MemoryPoolState"/> found or <see cref="Memory{MemoryPoolState}.Empty"/> if not found.</returns> + private Span<MemoryPoolState> FindMemoryPool(CpuAddress cpuAddress, ulong size) + { + if (!_memoryPools.IsEmpty && _memoryPools.Length > 0) + { + for (int i = 0; i < _memoryPools.Length; i++) + { + if (_memoryPools.Span[i].Contains(cpuAddress, size)) + { + return _memoryPools.Span.Slice(i, 1); + } + } + } + + return Span<MemoryPoolState>.Empty; + } + + /// <summary> + /// Force unmap the given <see cref="AddressInfo"/>. + /// </summary> + /// <param name="addressInfo">The <see cref="AddressInfo"/> to force unmap</param> + public void ForceUnmap(ref AddressInfo addressInfo) + { + if (_isForceMapEnabled) + { + Span<MemoryPoolState> memoryPool = FindMemoryPool(addressInfo.CpuAddress, addressInfo.Size); + + if (!memoryPool.IsEmpty) + { + AudioProcessorMemoryManager.Unmap(_processHandle, memoryPool[0].CpuAddress, memoryPool[0].Size); + + return; + } + + AudioProcessorMemoryManager.Unmap(_processHandle, addressInfo.CpuAddress, 0); + } + } + + /// <summary> + /// Try to attach the given region to the <see cref="AddressInfo"/>. + /// </summary> + /// <param name="errorInfo">The error information if an error was generated.</param> + /// <param name="addressInfo">The <see cref="AddressInfo"/> to attach the region to.</param> + /// <param name="cpuAddress">The region <see cref="CpuAddress"/>.</param> + /// <param name="size">The region size.</param> + /// <returns>Returns true if mapping was performed.</returns> + public bool TryAttachBuffer(out ErrorInfo errorInfo, ref AddressInfo addressInfo, CpuAddress cpuAddress, ulong size) + { + errorInfo = new ErrorInfo(); + + addressInfo.Setup(cpuAddress, size); + + if (AssignDspAddress(ref addressInfo)) + { + errorInfo.ErrorCode = 0x0; + errorInfo.ExtraErrorInfo = 0x0; + + return true; + } + else + { + errorInfo.ErrorCode = ResultCode.InvalidAddressInfo; + errorInfo.ExtraErrorInfo = addressInfo.CpuAddress; + + return _isForceMapEnabled; + } + } + + /// <summary> + /// Update a <see cref="MemoryPoolState"/> using user parameters. + /// </summary> + /// <param name="memoryPool">The <see cref="MemoryPoolState"/> to update.</param> + /// <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) + { + MemoryPoolUserState inputState = inParameter.State; + + MemoryPoolUserState outputState; + + const uint pageSize = 0x1000; + + if (inputState != MemoryPoolUserState.RequestAttach && inputState != MemoryPoolUserState.RequestDetach) + { + return UpdateResult.Success; + } + + if (inParameter.CpuAddress == 0 || (inParameter.CpuAddress & (pageSize - 1)) != 0) + { + return UpdateResult.InvalidParameter; + } + + if (inParameter.Size == 0 || (inParameter.Size & (pageSize - 1)) != 0) + { + return UpdateResult.InvalidParameter; + } + + if (inputState == MemoryPoolUserState.RequestAttach) + { + bool initializeSuccess = InitializePool(ref memoryPool, inParameter.CpuAddress, inParameter.Size); + + if (!initializeSuccess) + { + memoryPool.SetCpuAddress(0, 0); + + Logger.Error?.Print(LogClass.AudioRenderer, $"Map of memory pool (address: 0x{inParameter.CpuAddress:x}, size 0x{inParameter.Size:x}) failed!"); + return UpdateResult.MapError; + } + + outputState = MemoryPoolUserState.Attached; + } + else + { + if (memoryPool.CpuAddress != inParameter.CpuAddress || memoryPool.Size != inParameter.Size) + { + return UpdateResult.InvalidParameter; + } + + if (!Unmap(ref memoryPool)) + { + Logger.Error?.Print(LogClass.AudioRenderer, $"Unmap of memory pool (address: 0x{memoryPool.CpuAddress:x}, size 0x{memoryPool.Size:x}) failed!"); + return UpdateResult.UnmapError; + } + + outputState = MemoryPoolUserState.Detached; + } + + outStatus.State = outputState; + + return UpdateResult.Success; + } + + /// <summary> + /// Map the <see cref="AddressInfo"/> to the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + /// <param name="addressInfo">The <see cref="AddressInfo"/> to map.</param> + /// <returns>Returns true if mapping was performed.</returns> + private bool AssignDspAddress(ref AddressInfo addressInfo) + { + if (addressInfo.CpuAddress == 0) + { + return false; + } + + if (_memoryPools.Length > 0) + { + Span<MemoryPoolState> memoryPool = FindMemoryPool(addressInfo.CpuAddress, addressInfo.Size); + + if (!memoryPool.IsEmpty) + { + addressInfo.SetupMemoryPool(memoryPool); + + return true; + } + } + + if (_isForceMapEnabled) + { + DspAddress dspAddress = AudioProcessorMemoryManager.Map(_processHandle, addressInfo.CpuAddress, addressInfo.Size); + + addressInfo.ForceMappedDspAddress = dspAddress; + + AudioProcessorMemoryManager.Map(_processHandle, addressInfo.CpuAddress, addressInfo.Size); + } + else + { + unsafe + { + addressInfo.SetupMemoryPool(MemoryPoolState.Null); + } + } + + return false; + } + + /// <summary> + /// Remove the usage flag from all the <see cref="MemoryPoolState"/>. + /// </summary> + /// <param name="memoryPool">The <see cref="Memory{MemoryPoolState}"/> to reset.</param> + public static void ClearUsageState(Memory<MemoryPoolState> memoryPool) + { + foreach (ref MemoryPoolState info in memoryPool.Span) + { + info.IsUsed = false; + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Mix/MixContext.cs b/Ryujinx.Audio/Renderer/Server/Mix/MixContext.cs new file mode 100644 index 00000000..0e094665 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Mix/MixContext.cs @@ -0,0 +1,276 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Server.Splitter; +using Ryujinx.Audio.Renderer.Utils; +using System; +using System.Diagnostics; + +namespace Ryujinx.Audio.Renderer.Server.Mix +{ + /// <summary> + /// Mix context. + /// </summary> + public class MixContext + { + /// <summary> + /// The total mix count. + /// </summary> + private uint _mixesCount; + + /// <summary> + /// Storage for <see cref="MixState"/>. + /// </summary> + private Memory<MixState> _mixes; + + /// <summary> + /// Storage of the sorted indices to <see cref="MixState"/>. + /// </summary> + private Memory<int> _sortedMixes; + + /// <summary> + /// Graph state. + /// </summary> + public NodeStates NodeStates { get; } + + /// <summary> + /// The instance of the adjacent matrix. + /// </summary> + public EdgeMatrix EdgeMatrix { get; } + + /// <summary> + /// Create a new instance of <see cref="MixContext"/>. + /// </summary> + public MixContext() + { + NodeStates = new NodeStates(); + EdgeMatrix = new EdgeMatrix(); + } + + /// <summary> + /// Initialize the <see cref="MixContext"/>. + /// </summary> + /// <param name="sortedMixes">The storage for sorted indices.</param> + /// <param name="mixes">The storage of <see cref="MixState"/>.</param> + /// <param name="nodeStatesWorkBuffer">The storage used for the <see cref="NodeStates"/>.</param> + /// <param name="edgeMatrixWorkBuffer">The storage used for the <see cref="EdgeMatrix"/>.</param> + public void Initialize(Memory<int> sortedMixes, Memory<MixState> mixes, Memory<byte> nodeStatesWorkBuffer, Memory<byte> edgeMatrixWorkBuffer) + { + _mixesCount = (uint)mixes.Length; + _mixes = mixes; + _sortedMixes = sortedMixes; + + if (!nodeStatesWorkBuffer.IsEmpty && !edgeMatrixWorkBuffer.IsEmpty) + { + NodeStates.Initialize(nodeStatesWorkBuffer, mixes.Length); + EdgeMatrix.Initialize(edgeMatrixWorkBuffer, mixes.Length); + } + + int sortedId = 0; + for (int i = 0; i < _mixes.Length; i++) + { + SetSortedState(sortedId++, i); + } + } + + /// <summary> + /// Associate the given <paramref name="targetIndex"/> to a given <paramref cref="id"/>. + /// </summary> + /// <param name="id">The sorted id.</param> + /// <param name="targetIndex">The index to associate.</param> + private void SetSortedState(int id, int targetIndex) + { + _sortedMixes.Span[id] = targetIndex; + } + + /// <summary> + /// Get a reference to the final <see cref="MixState"/>. + /// </summary> + /// <returns>A reference to the final <see cref="MixState"/>.</returns> + public ref MixState GetFinalState() + { + return ref GetState(Constants.FinalMixId); + } + + /// <summary> + /// Get a reference to a <see cref="MixState"/> at the given <paramref name="id"/>. + /// </summary> + /// <param name="id">The index to use.</param> + /// <returns>A reference to a <see cref="MixState"/> at the given <paramref name="id"/>.</returns> + public ref MixState GetState(int id) + { + return ref SpanIOHelper.GetFromMemory(_mixes, id, _mixesCount); + } + + /// <summary> + /// Get a reference to a <see cref="MixState"/> at the given <paramref name="id"/> of the sorted mix info. + /// </summary> + /// <param name="id">The index to use.</param> + /// <returns>A reference to a <see cref="MixState"/> at the given <paramref name="id"/>.</returns> + public ref MixState GetSortedState(int id) + { + Debug.Assert(id >= 0 && id < _mixesCount); + + return ref GetState(_sortedMixes.Span[id]); + } + + /// <summary> + /// Get the total mix count. + /// </summary> + /// <returns>The total mix count.</returns> + public uint GetCount() + { + return _mixesCount; + } + + /// <summary> + /// Update the internal distance from the final mix value of every <see cref="MixState"/>. + /// </summary> + private void UpdateDistancesFromFinalMix() + { + foreach (ref MixState mix in _mixes.Span) + { + mix.ClearDistanceFromFinalMix(); + } + + for (int i = 0; i < GetCount(); i++) + { + ref MixState mix = ref GetState(i); + + SetSortedState(i, i); + + if (mix.IsUsed) + { + uint distance; + + if (mix.MixId != Constants.FinalMixId) + { + int mixId = mix.MixId; + + for (distance = 0; distance < GetCount(); distance++) + { + if (mixId == Constants.UnusedMixId) + { + distance = MixState.InvalidDistanceFromFinalMix; + break; + } + + ref MixState distanceMix = ref GetState(mixId); + + if (distanceMix.DistanceFromFinalMix != MixState.InvalidDistanceFromFinalMix) + { + distance = distanceMix.DistanceFromFinalMix + 1; + break; + } + + mixId = distanceMix.DestinationMixId; + + if (mixId == Constants.FinalMixId) + { + break; + } + } + + if (distance > GetCount()) + { + distance = MixState.InvalidDistanceFromFinalMix; + } + } + else + { + distance = MixState.InvalidDistanceFromFinalMix; + } + + mix.DistanceFromFinalMix = distance; + } + } + } + + /// <summary> + /// Update the internal mix buffer offset of all <see cref="MixState"/>. + /// </summary> + private void UpdateMixBufferOffset() + { + uint offset = 0; + + foreach (ref MixState mix in _mixes.Span) + { + mix.BufferOffset = offset; + + offset += mix.BufferCount; + } + } + + /// <summary> + /// Sort the mixes using distance from the final mix. + /// </summary> + public void Sort() + { + UpdateDistancesFromFinalMix(); + + int[] sortedMixesTemp = _sortedMixes.Slice(0, (int)GetCount()).ToArray(); + + Array.Sort(sortedMixesTemp, (a, b) => + { + ref MixState stateA = ref GetState(a); + ref MixState stateB = ref GetState(b); + + return stateB.DistanceFromFinalMix.CompareTo(stateA.DistanceFromFinalMix); + }); + + sortedMixesTemp.AsSpan().CopyTo(_sortedMixes.Span); + + UpdateMixBufferOffset(); + } + + /// <summary> + /// Sort the mixes and splitters using an adjacency matrix. + /// </summary> + /// <param name="splitterContext">The <see cref="SplitterContext"/> used.</param> + /// <returns>Return true, if no errors in the graph were detected.</returns> + public bool Sort(SplitterContext splitterContext) + { + if (splitterContext.UsingSplitter()) + { + bool isValid = NodeStates.Sort(EdgeMatrix); + + if (isValid) + { + ReadOnlySpan<int> sortedMixesIndex = NodeStates.GetTsortResult(); + + int id = 0; + + for (int i = sortedMixesIndex.Length - 1; i >= 0; i--) + { + SetSortedState(id++, sortedMixesIndex[i]); + } + + UpdateMixBufferOffset(); + } + + return isValid; + } + else + { + UpdateMixBufferOffset(); + + return true; + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Mix/MixState.cs b/Ryujinx.Audio/Renderer/Server/Mix/MixState.cs new file mode 100644 index 00000000..028d0885 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Mix/MixState.cs @@ -0,0 +1,330 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Server.Effect; +using Ryujinx.Audio.Renderer.Server.Splitter; +using Ryujinx.Common.Utilities; +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using static Ryujinx.Audio.Constants; + +namespace Ryujinx.Audio.Renderer.Server.Mix +{ + /// <summary> + /// Server state for a mix. + /// </summary> + [StructLayout(LayoutKind.Sequential, Size = 0x940, Pack = Alignment)] + public struct MixState + { + public const uint InvalidDistanceFromFinalMix = 0x80000000; + + public const int Alignment = 0x10; + + /// <summary> + /// Base volume of the mix. + /// </summary> + public float Volume; + + /// <summary> + /// Target sample rate of the mix. + /// </summary> + public uint SampleRate; + + /// <summary> + /// Target buffer count. + /// </summary> + public uint BufferCount; + + /// <summary> + /// Set to true if in use. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool IsUsed; + + /// <summary> + /// The id of the mix. + /// </summary> + public int MixId; + + /// <summary> + /// The mix node id. + /// </summary> + public int NodeId; + + /// <summary> + /// the buffer offset to use for command generation. + /// </summary> + public uint BufferOffset; + + /// <summary> + /// The distance of the mix from the final mix. + /// </summary> + public uint DistanceFromFinalMix; + + /// <summary> + /// The effect processing order storage. + /// </summary> + private IntPtr _effectProcessingOrderArrayPointer; + + /// <summary> + /// The max element count that can be found in the effect processing order storage. + /// </summary> + public uint EffectProcessingOrderArrayMaxCount; + + /// <summary> + /// The mix to output the result of this mix. + /// </summary> + public int DestinationMixId; + + /// <summary> + /// Mix buffer volumes storage. + /// </summary> + private MixVolumeArray _mixVolumeArray; + + /// <summary> + /// The splitter to output the result of this mix. + /// </summary> + public uint DestinationSplitterId; + + /// <summary> + /// If set to true, the long size pre-delay is supported on the reverb command. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool IsLongSizePreDelaySupported; + + [StructLayout(LayoutKind.Sequential, Size = Size, Pack = 1)] + private struct MixVolumeArray + { + private const int Size = 4 * MixBufferCountMax * MixBufferCountMax; + } + + /// <summary> + /// Mix buffer volumes. + /// </summary> + /// <remarks>Used when no splitter id is specified.</remarks> + public Span<float> MixBufferVolume => SpanHelpers.AsSpan<MixVolumeArray, float>(ref _mixVolumeArray); + + /// <summary> + /// Get the volume for a given connection destination. + /// </summary> + /// <param name="sourceIndex">The source node index.</param> + /// <param name="destinationIndex">The destination node index</param> + /// <returns>The volume for the given connection destination.</returns> + public float GetMixBufferVolume(int sourceIndex, int destinationIndex) + { + return MixBufferVolume[sourceIndex * MixBufferCountMax + destinationIndex]; + } + + /// <summary> + /// The array used to order effects associated to this mix. + /// </summary> + public Span<int> EffectProcessingOrderArray + { + get + { + if (_effectProcessingOrderArrayPointer == IntPtr.Zero) + { + return Span<int>.Empty; + } + + unsafe + { + return new Span<int>((void*)_effectProcessingOrderArrayPointer, (int)EffectProcessingOrderArrayMaxCount); + } + } + } + + /// <summary> + /// Create a new <see cref="MixState"/> + /// </summary> + /// <param name="effectProcessingOrderArray"></param> + /// <param name="behaviourContext"></param> + public MixState(Memory<int> effectProcessingOrderArray, ref BehaviourContext behaviourContext) : this() + { + MixId = UnusedMixId; + + DistanceFromFinalMix = InvalidDistanceFromFinalMix; + + DestinationMixId = UnusedMixId; + + DestinationSplitterId = UnusedSplitterId; + + unsafe + { + // SAFETY: safe as effectProcessingOrderArray comes from the work buffer memory that is pinned. + _effectProcessingOrderArrayPointer = (IntPtr)Unsafe.AsPointer(ref MemoryMarshal.GetReference(effectProcessingOrderArray.Span)); + } + + EffectProcessingOrderArrayMaxCount = (uint)effectProcessingOrderArray.Length; + + IsLongSizePreDelaySupported = behaviourContext.IsLongSizePreDelaySupported(); + + ClearEffectProcessingOrder(); + } + + /// <summary> + /// Clear the <see cref="DistanceFromFinalMix"/> value to its default state. + /// </summary> + public void ClearDistanceFromFinalMix() + { + DistanceFromFinalMix = InvalidDistanceFromFinalMix; + } + + /// <summary> + /// Clear the <see cref="EffectProcessingOrderArray"/> to its default state. + /// </summary> + public void ClearEffectProcessingOrder() + { + EffectProcessingOrderArray.Fill(-1); + } + + /// <summary> + /// Return true if the mix has any destinations. + /// </summary> + /// <returns>True if the mix has any destinations.</returns> + public bool HasAnyDestination() + { + return DestinationMixId != UnusedMixId || DestinationSplitterId != UnusedSplitterId; + } + + /// <summary> + /// Update the mix connection on the adjacency matrix. + /// </summary> + /// <param name="edgeMatrix">The adjacency matrix.</param> + /// <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) + { + bool hasNewConnections; + + if (DestinationSplitterId == UnusedSplitterId) + { + hasNewConnections = false; + } + else + { + ref SplitterState splitter = ref splitterContext.GetState((int)DestinationSplitterId); + + hasNewConnections = splitter.HasNewConnection; + } + + if (DestinationMixId == parameter.DestinationMixId && DestinationSplitterId == parameter.DestinationSplitterId && !hasNewConnections) + { + return false; + } + + edgeMatrix.RemoveEdges(MixId); + + if (parameter.DestinationMixId == UnusedMixId) + { + if (parameter.DestinationSplitterId != UnusedSplitterId) + { + ref SplitterState splitter = ref splitterContext.GetState((int)parameter.DestinationSplitterId); + + for (int i = 0; i < splitter.DestinationCount; i++) + { + Span<SplitterDestination> destination = splitter.GetData(i); + + if (!destination.IsEmpty) + { + int destinationMixId = destination[0].DestinationId; + + if (destinationMixId != UnusedMixId) + { + edgeMatrix.Connect(MixId, destinationMixId); + } + } + } + } + } + else + { + edgeMatrix.Connect(MixId, parameter.DestinationMixId); + } + + DestinationMixId = parameter.DestinationMixId; + DestinationSplitterId = parameter.DestinationSplitterId; + + return true; + } + + /// <summary> + /// Update the mix from user information. + /// </summary> + /// <param name="edgeMatrix">The adjacency matrix.</param> + /// <param name="parameter">The input parameter of the mix.</param> + /// <param name="effectContext">The effect context.</param> + /// <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) + { + bool isDirty; + + Volume = parameter.Volume; + SampleRate = parameter.SampleRate; + BufferCount = parameter.BufferCount; + IsUsed = parameter.IsUsed; + MixId = parameter.MixId; + NodeId = parameter.NodeId; + parameter.MixBufferVolume.CopyTo(MixBufferVolume); + + if (behaviourContext.IsSplitterSupported()) + { + isDirty = UpdateConnection(edgeMatrix, ref parameter, ref splitterContext); + } + else + { + isDirty = DestinationMixId != parameter.DestinationMixId; + + if (DestinationMixId != parameter.DestinationMixId) + { + DestinationMixId = parameter.DestinationMixId; + } + + DestinationSplitterId = UnusedSplitterId; + } + + ClearEffectProcessingOrder(); + + for (int i = 0; i < effectContext.GetCount(); i++) + { + ref BaseEffect effect = ref effectContext.GetEffect(i); + + if (effect.MixId == MixId) + { + Debug.Assert(effect.ProcessingOrder <= EffectProcessingOrderArrayMaxCount); + + if (effect.ProcessingOrder > EffectProcessingOrderArrayMaxCount) + { + return isDirty; + } + + EffectProcessingOrderArray[(int)effect.ProcessingOrder] = i; + } + } + + return isDirty; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Performance/IPerformanceDetailEntry.cs b/Ryujinx.Audio/Renderer/Server/Performance/IPerformanceDetailEntry.cs new file mode 100644 index 00000000..893cc008 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Performance/IPerformanceDetailEntry.cs @@ -0,0 +1,69 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; + +namespace Ryujinx.Audio.Renderer.Server.Performance +{ + /// <summary> + /// Represents a detailed entry in a performance frame. + /// </summary> + public interface IPerformanceDetailEntry + { + /// <summary> + /// Get the start time of this entry event (in microseconds). + /// </summary> + /// <returns>The start time of this entry event (in microseconds).</returns> + int GetStartTime(); + + /// <summary> + /// Get the start time offset in this structure. + /// </summary> + /// <returns>The start time offset in this structure.</returns> + int GetStartTimeOffset(); + + /// <summary> + /// Get the processing time of this entry event (in microseconds). + /// </summary> + /// <returns>The processing time of this entry event (in microseconds).</returns> + int GetProcessingTime(); + + /// <summary> + /// Get the processing time offset in this structure. + /// </summary> + /// <returns>The processing time offset in this structure.</returns> + int GetProcessingTimeOffset(); + + /// <summary> + /// Set the <paramref name="nodeId"/> of this entry. + /// </summary> + /// <param name="nodeId">The node id of this entry.</param> + void SetNodeId(int nodeId); + + /// <summary> + /// Set the <see cref="PerformanceEntryType"/> of this entry. + /// </summary> + /// <param name="type">The type to use.</param> + void SetEntryType(PerformanceEntryType type); + + /// <summary> + /// Set the <see cref="PerformanceDetailType"/> of this entry. + /// </summary> + /// <param name="detailType">The type to use.</param> + void SetDetailType(PerformanceDetailType detailType); + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Performance/IPerformanceEntry.cs b/Ryujinx.Audio/Renderer/Server/Performance/IPerformanceEntry.cs new file mode 100644 index 00000000..2a2fa9cc --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Performance/IPerformanceEntry.cs @@ -0,0 +1,63 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; + +namespace Ryujinx.Audio.Renderer.Server.Performance +{ + /// <summary> + /// Represents an entry in a performance frame. + /// </summary> + public interface IPerformanceEntry + { + /// <summary> + /// Get the start time of this entry event (in microseconds). + /// </summary> + /// <returns>The start time of this entry event (in microseconds).</returns> + int GetStartTime(); + + /// <summary> + /// Get the start time offset in this structure. + /// </summary> + /// <returns>The start time offset in this structure.</returns> + int GetStartTimeOffset(); + + /// <summary> + /// Get the processing time of this entry event (in microseconds). + /// </summary> + /// <returns>The processing time of this entry event (in microseconds).</returns> + int GetProcessingTime(); + + /// <summary> + /// Get the processing time offset in this structure. + /// </summary> + /// <returns>The processing time offset in this structure.</returns> + int GetProcessingTimeOffset(); + + /// <summary> + /// Set the <paramref name="nodeId"/> of this entry. + /// </summary> + /// <param name="nodeId">The node id of this entry.</param> + void SetNodeId(int nodeId); + + /// <summary> + /// Set the <see cref="PerformanceEntryType"/> of this entry. + /// </summary> + /// <param name="type">The type to use.</param> + void SetEntryType(PerformanceEntryType type); + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Performance/IPerformanceHeader.cs b/Ryujinx.Audio/Renderer/Server/Performance/IPerformanceHeader.cs new file mode 100644 index 00000000..5a26754d --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Performance/IPerformanceHeader.cs @@ -0,0 +1,97 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +namespace Ryujinx.Audio.Renderer.Server.Performance +{ + /// <summary> + /// The header of a performance frame. + /// </summary> + public interface IPerformanceHeader + { + /// <summary> + /// Get the entry count offset in this structure. + /// </summary> + /// <returns>The entry count offset in this structure.</returns> + int GetEntryCountOffset(); + + /// <summary> + /// Set the DSP running behind flag. + /// </summary> + /// <param name="isRunningBehind">The flag.</param> + void SetDspRunningBehind(bool isRunningBehind); + + /// <summary> + /// Set the count of voices that were dropped. + /// </summary> + /// <param name="voiceCount">The count of voices that were dropped.</param> + void SetVoiceDropCount(uint voiceCount); + + /// <summary> + /// Set the start ticks of the <see cref="Dsp.AudioProcessor"/>. (before sending commands) + /// </summary> + /// <param name="startTicks">The start ticks of the <see cref="Dsp.AudioProcessor"/>. (before sending commands)</param> + void SetStartRenderingTicks(ulong startTicks); + + /// <summary> + /// Set the header magic. + /// </summary> + /// <param name="magic">The header magic.</param> + void SetMagic(uint magic); + + /// <summary> + /// Set the offset of the next performance header. + /// </summary> + /// <param name="nextOffset">The offset of the next performance header.</param> + void SetNextOffset(int nextOffset); + + /// <summary> + /// Set the total time taken by all the commands profiled. + /// </summary> + /// <param name="totalProcessingTime">The total time taken by all the commands profiled.</param> + void SetTotalProcessingTime(int totalProcessingTime); + + /// <summary> + /// Set the index of this performance frame. + /// </summary> + /// <param name="index">The index of this performance frame.</param> + void SetIndex(uint index); + + /// <summary> + /// Get the total count of entries in this frame. + /// </summary> + /// <returns>The total count of entries in this frame.</returns> + int GetEntryCount(); + + /// <summary> + /// Get the total count of detailed entries in this frame. + /// </summary> + /// <returns>The total count of detailed entries in this frame.</returns> + int GetEntryDetailCount(); + + /// <summary> + /// Set the total count of entries in this frame. + /// </summary> + /// <param name="entryCount">The total count of entries in this frame.</param> + void SetEntryCount(int entryCount); + + /// <summary> + /// Set the total count of detailed entries in this frame. + /// </summary> + /// <param name="entryDetailCount">The total count of detailed entries in this frame.</param> + void SetEntryDetailCount(int entryDetailCount); + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Performance/PerformanceDetailVersion1.cs b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceDetailVersion1.cs new file mode 100644 index 00000000..b7df7f28 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceDetailVersion1.cs @@ -0,0 +1,89 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Performance +{ + /// <summary> + /// Implementation of <see cref="IPerformanceDetailEntry"/> for performance metrics version 1. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x10)] + public struct PerformanceDetailVersion1 : IPerformanceDetailEntry + { + /// <summary> + /// The node id associated to this detailed entry. + /// </summary> + public int NodeId; + + /// <summary> + /// The start time (in microseconds) associated to this detailed entry. + /// </summary> + public int StartTime; + + /// <summary> + /// The processing time (in microseconds) associated to this detailed entry. + /// </summary> + public int ProcessingTime; + + /// <summary> + /// The detailed entry type associated to this detailed entry. + /// </summary> + public PerformanceDetailType DetailType; + + /// <summary> + /// The entry type associated to this detailed entry. + /// </summary> + public PerformanceEntryType EntryType; + + public int GetProcessingTime() + { + return ProcessingTime; + } + + public int GetProcessingTimeOffset() + { + return 8; + } + + public int GetStartTime() + { + return StartTime; + } + + public int GetStartTimeOffset() + { + return 4; + } + + public void SetDetailType(PerformanceDetailType detailType) + { + DetailType = detailType; + } + + public void SetEntryType(PerformanceEntryType type) + { + EntryType = type; + } + + public void SetNodeId(int nodeId) + { + NodeId = nodeId; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Performance/PerformanceDetailVersion2.cs b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceDetailVersion2.cs new file mode 100644 index 00000000..da898b29 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceDetailVersion2.cs @@ -0,0 +1,89 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Performance +{ + /// <summary> + /// Implementation of <see cref="IPerformanceDetailEntry"/> for performance metrics version 2. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x18)] + public struct PerformanceDetailVersion2 : IPerformanceDetailEntry + { + /// <summary> + /// The node id associated to this detailed entry. + /// </summary> + public int NodeId; + + /// <summary> + /// The start time (in microseconds) associated to this detailed entry. + /// </summary> + public int StartTime; + + /// <summary> + /// The processing time (in microseconds) associated to this detailed entry. + /// </summary> + public int ProcessingTime; + + /// <summary> + /// The detailed entry type associated to this detailed entry. + /// </summary> + public PerformanceDetailType DetailType; + + /// <summary> + /// The entry type associated to this detailed entry. + /// </summary> + public PerformanceEntryType EntryType; + + public int GetProcessingTime() + { + return ProcessingTime; + } + + public int GetProcessingTimeOffset() + { + return 8; + } + + public int GetStartTime() + { + return StartTime; + } + + public int GetStartTimeOffset() + { + return 4; + } + + public void SetDetailType(PerformanceDetailType detailType) + { + DetailType = detailType; + } + + public void SetEntryType(PerformanceEntryType type) + { + EntryType = type; + } + + public void SetNodeId(int nodeId) + { + NodeId = nodeId; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Performance/PerformanceEntryAddresses.cs b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceEntryAddresses.cs new file mode 100644 index 00000000..ad99867f --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceEntryAddresses.cs @@ -0,0 +1,73 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using System; + +namespace Ryujinx.Audio.Renderer.Server.Performance +{ + /// <summary> + /// Information used by the performance command to store informations in the performance entry. + /// </summary> + public class PerformanceEntryAddresses + { + /// <summary> + /// The memory storing the performance entry. + /// </summary> + public Memory<int> BaseMemory; + + /// <summary> + /// The offset to the start time field. + /// </summary> + public uint StartTimeOffset; + + /// <summary> + /// The offset to the entry count field. + /// </summary> + public uint EntryCountOffset; + + /// <summary> + /// The offset to the processing time field. + /// </summary> + public uint ProcessingTimeOffset; + + /// <summary> + /// Increment the entry count. + /// </summary> + public void IncrementEntryCount() + { + BaseMemory.Span[(int)EntryCountOffset / 4]++; + } + + /// <summary> + /// Set the start time in the entry. + /// </summary> + /// <param name="startTimeNano">The start time in nanoseconds.</param> + public void SetStartTime(ulong startTimeNano) + { + BaseMemory.Span[(int)StartTimeOffset / 4] = (int)(startTimeNano / 1000); + } + + /// <summary> + /// Set the processing time in the entry. + /// </summary> + /// <param name="endTimeNano">The end time in nanoseconds.</param> + public void SetProcessingTime(ulong endTimeNano) + { + BaseMemory.Span[(int)ProcessingTimeOffset / 4] = (int)(endTimeNano / 1000) - BaseMemory.Span[(int)StartTimeOffset / 4]; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Performance/PerformanceEntryVersion1.cs b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceEntryVersion1.cs new file mode 100644 index 00000000..ac91ca19 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceEntryVersion1.cs @@ -0,0 +1,79 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Performance +{ + /// <summary> + /// Implementation of <see cref="IPerformanceEntry"/> for performance metrics version 1. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x10)] + public struct PerformanceEntryVersion1 : IPerformanceEntry + { + /// <summary> + /// The node id associated to this entry. + /// </summary> + public int NodeId; + + /// <summary> + /// The start time (in microseconds) associated to this entry. + /// </summary> + public int StartTime; + + /// <summary> + /// The processing time (in microseconds) associated to this entry. + /// </summary> + public int ProcessingTime; + + /// <summary> + /// The entry type associated to this entry. + /// </summary> + public PerformanceEntryType EntryType; + + public int GetProcessingTime() + { + return ProcessingTime; + } + + public int GetProcessingTimeOffset() + { + return 8; + } + + public int GetStartTime() + { + return StartTime; + } + + public int GetStartTimeOffset() + { + return 4; + } + + public void SetEntryType(PerformanceEntryType type) + { + EntryType = type; + } + + public void SetNodeId(int nodeId) + { + NodeId = nodeId; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Performance/PerformanceEntryVersion2.cs b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceEntryVersion2.cs new file mode 100644 index 00000000..84ee5bc9 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceEntryVersion2.cs @@ -0,0 +1,79 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Performance +{ + /// <summary> + /// Implementation of <see cref="IPerformanceEntry"/> for performance metrics version 2. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x18)] + public struct PerformanceEntryVersion2 : IPerformanceEntry + { + /// <summary> + /// The node id associated to this entry. + /// </summary> + public int NodeId; + + /// <summary> + /// The start time (in microseconds) associated to this entry. + /// </summary> + public int StartTime; + + /// <summary> + /// The processing time (in microseconds) associated to this entry. + /// </summary> + public int ProcessingTime; + + /// <summary> + /// The entry type associated to this entry. + /// </summary> + public PerformanceEntryType EntryType; + + public int GetProcessingTime() + { + return ProcessingTime; + } + + public int GetProcessingTimeOffset() + { + return 8; + } + + public int GetStartTime() + { + return StartTime; + } + + public int GetStartTimeOffset() + { + return 4; + } + + public void SetEntryType(PerformanceEntryType type) + { + EntryType = type; + } + + public void SetNodeId(int nodeId) + { + NodeId = nodeId; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Performance/PerformanceFrameHeaderVersion1.cs b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceFrameHeaderVersion1.cs new file mode 100644 index 00000000..b0757b37 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceFrameHeaderVersion1.cs @@ -0,0 +1,118 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Performance +{ + /// <summary> + /// Implementation of <see cref="IPerformanceHeader"/> for performance metrics version 1. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x18)] + public struct PerformanceFrameHeaderVersion1 : IPerformanceHeader + { + /// <summary> + /// The magic of the performance header. + /// </summary> + public uint Magic; + + /// <summary> + /// The total count of entries in this frame. + /// </summary> + public int EntryCount; + + /// <summary> + /// The total count of detailed entries in this frame. + /// </summary> + public int EntryDetailCount; + + /// <summary> + /// The offset of the next performance header. + /// </summary> + public int NextOffset; + + /// <summary> + /// The total time taken by all the commands profiled. + /// </summary> + public int TotalProcessingTime; + + /// <summary> + /// The count of voices that were dropped. + /// </summary> + public uint VoiceDropCount; + + public int GetEntryCount() + { + return EntryCount; + } + + public int GetEntryCountOffset() + { + return 4; + } + + public int GetEntryDetailCount() + { + return EntryDetailCount; + } + + public void SetDspRunningBehind(bool isRunningBehind) + { + // NOTE: Not present in version 1 + } + + public void SetEntryCount(int entryCount) + { + EntryCount = entryCount; + } + + public void SetEntryDetailCount(int entryDetailCount) + { + EntryDetailCount = entryDetailCount; + } + + public void SetIndex(uint index) + { + // NOTE: Not present in version 1 + } + + public void SetMagic(uint magic) + { + Magic = magic; + } + + public void SetNextOffset(int nextOffset) + { + NextOffset = nextOffset; + } + + public void SetStartRenderingTicks(ulong startTicks) + { + // NOTE: not present in version 1 + } + + public void SetTotalProcessingTime(int totalProcessingTime) + { + TotalProcessingTime = totalProcessingTime; + } + + public void SetVoiceDropCount(uint voiceCount) + { + VoiceDropCount = voiceCount; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Performance/PerformanceFrameHeaderVersion2.cs b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceFrameHeaderVersion2.cs new file mode 100644 index 00000000..ba873239 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceFrameHeaderVersion2.cs @@ -0,0 +1,134 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Performance +{ + /// <summary> + /// Implementation of <see cref="IPerformanceHeader"/> for performance metrics version 2. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x30)] + public struct PerformanceFrameHeaderVersion2 : IPerformanceHeader + { + /// <summary> + /// The magic of the performance header. + /// </summary> + public uint Magic; + + /// <summary> + /// The total count of entries in this frame. + /// </summary> + public int EntryCount; + + /// <summary> + /// The total count of detailed entries in this frame. + /// </summary> + public int EntryDetailCount; + + /// <summary> + /// The offset of the next performance header. + /// </summary> + public int NextOffset; + + /// <summary> + /// The total time taken by all the commands profiled. + /// </summary> + public int TotalProcessingTime; + + /// <summary> + /// The count of voices that were dropped. + /// </summary> + public uint VoiceDropCount; + + /// <summary> + /// The start ticks of the <see cref="Dsp.AudioProcessor"/>. (before sending commands) + /// </summary> + public ulong StartRenderingTicks; + + /// <summary> + /// The index of this performance frame. + /// </summary> + public uint Index; + + /// <summary> + /// If set to true, the DSP is running behind. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool IsDspRunningBehind; + + public int GetEntryCount() + { + return EntryCount; + } + + public int GetEntryCountOffset() + { + return 4; + } + + public int GetEntryDetailCount() + { + return EntryDetailCount; + } + + public void SetDspRunningBehind(bool isRunningBehind) + { + IsDspRunningBehind = isRunningBehind; + } + + public void SetEntryCount(int entryCount) + { + EntryCount = entryCount; + } + + public void SetEntryDetailCount(int entryDetailCount) + { + EntryDetailCount = entryDetailCount; + } + + public void SetIndex(uint index) + { + Index = index; + } + + public void SetMagic(uint magic) + { + Magic = magic; + } + + public void SetNextOffset(int nextOffset) + { + NextOffset = nextOffset; + } + + public void SetStartRenderingTicks(ulong startTicks) + { + StartRenderingTicks = startTicks; + } + + public void SetTotalProcessingTime(int totalProcessingTime) + { + TotalProcessingTime = totalProcessingTime; + } + + public void SetVoiceDropCount(uint voiceCount) + { + VoiceDropCount = voiceCount; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Performance/PerformanceManager.cs b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceManager.cs new file mode 100644 index 00000000..3a336af3 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceManager.cs @@ -0,0 +1,124 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Parameter; +using System; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Audio.Renderer.Server.Performance +{ + public abstract class PerformanceManager + { + /// <summary> + /// Get the required size for a single performance frame. + /// </summary> + /// <param name="parameter">The audio renderer configuration.</param> + /// <param name="behaviourContext">The behaviour context.</param> + /// <returns>The required size for a single performance frame.</returns> + public static ulong GetRequiredBufferSizeForPerformanceMetricsPerFrame(ref AudioRendererConfiguration parameter, ref BehaviourContext behaviourContext) + { + uint version = behaviourContext.GetPerformanceMetricsDataFormat(); + + if (version == 2) + { + return (ulong)PerformanceManagerGeneric<PerformanceFrameHeaderVersion2, + PerformanceEntryVersion2, + PerformanceDetailVersion2>.GetRequiredBufferSizeForPerformanceMetricsPerFrame(ref parameter); + } + else if (version == 1) + { + return (ulong)PerformanceManagerGeneric<PerformanceFrameHeaderVersion1, + PerformanceEntryVersion1, + PerformanceDetailVersion1>.GetRequiredBufferSizeForPerformanceMetricsPerFrame(ref parameter); + } + + throw new NotImplementedException($"Unknown Performance metrics data format version {version}"); + } + + /// <summary> + /// Copy the performance frame history to the supplied user buffer and returns the size copied. + /// </summary> + /// <param name="performanceOutput">The supplied user buffer to store the performance frame into.</param> + /// <returns>The size copied to the supplied buffer.</returns> + public abstract uint CopyHistories(Span<byte> performanceOutput); + + /// <summary> + /// Set the target node id to profile. + /// </summary> + /// <param name="target">The target node id to profile.</param> + public abstract void SetTargetNodeId(int target); + + /// <summary> + /// Check if the given target node id is profiled. + /// </summary> + /// <param name="target">The target node id to check.</param> + /// <returns>Return true, if the given target node id is profiled.</returns> + public abstract bool IsTargetNodeId(int target); + + /// <summary> + /// Get the next buffer to store a performance entry. + /// </summary> + /// <param name="performanceEntry">The output <see cref="PerformanceEntryAddresses"/>.</param> + /// <param name="entryType">The <see cref="PerformanceEntryType"/> info.</param> + /// <param name="nodeId">The node id of the entry.</param> + /// <returns>Return true, if a valid <see cref="PerformanceEntryAddresses"/> was returned.</returns> + public abstract bool GetNextEntry(out PerformanceEntryAddresses performanceEntry, PerformanceEntryType entryType, int nodeId); + + /// <summary> + /// Get the next buffer to store a performance detailed entry. + /// </summary> + /// <param name="performanceEntry">The output <see cref="PerformanceEntryAddresses"/>.</param> + /// <param name="detailType">The <see cref="PerformanceDetailType"/> info.</param> + /// <param name="entryType">The <see cref="PerformanceEntryType"/> info.</param> + /// <param name="nodeId">The node id of the entry.</param> + /// <returns>Return true, if a valid <see cref="PerformanceEntryAddresses"/> was returned.</returns> + public abstract bool GetNextEntry(out PerformanceEntryAddresses performanceEntry, PerformanceDetailType detailType, PerformanceEntryType entryType, int nodeId); + + /// <summary> + /// Finalize the current performance frame. + /// </summary> + /// <param name="dspRunningBehind">Indicate if the DSP is running behind.</param> + /// <param name="voiceDropCount">The count of voices that were dropped.</param> + /// <param name="startRenderingTicks">The start ticks of the audio rendering.</param> + public abstract void TapFrame(bool dspRunningBehind, uint voiceDropCount, ulong startRenderingTicks); + + /// <summary> + /// Create a new <see cref="PerformanceManager"/>. + /// </summary> + /// <param name="performanceBuffer">The backing memory available for use by the manager.</param> + /// <param name="parameter">The audio renderer configuration.</param> + /// <param name="behaviourContext">The behaviour context;</param> + /// <returns>A new <see cref="PerformanceManager"/>.</returns> + public static PerformanceManager Create(Memory<byte> performanceBuffer, ref AudioRendererConfiguration parameter, BehaviourContext behaviourContext) + { + uint version = behaviourContext.GetPerformanceMetricsDataFormat(); + + switch (version) + { + case 1: + return new PerformanceManagerGeneric<PerformanceFrameHeaderVersion1, PerformanceEntryVersion1, PerformanceDetailVersion1>(performanceBuffer, + ref parameter); + case 2: + return new PerformanceManagerGeneric<PerformanceFrameHeaderVersion2, PerformanceEntryVersion2, PerformanceDetailVersion2>(performanceBuffer, + ref parameter); + default: + throw new NotImplementedException($"Unknown Performance metrics data format version {version}"); + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Performance/PerformanceManagerGeneric.cs b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceManagerGeneric.cs new file mode 100644 index 00000000..b17e6f3f --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceManagerGeneric.cs @@ -0,0 +1,311 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Utils; +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Performance +{ + /// <summary> + /// A Generic implementation of <see cref="PerformanceManager"/>. + /// </summary> + /// <typeparam name="THeader">The header implementation of the performance frame.</typeparam> + /// <typeparam name="TEntry">The entry implementation of the performance frame.</typeparam> + /// <typeparam name="TEntryDetail">A detailed implementation of the performance frame.</typeparam> + public class PerformanceManagerGeneric<THeader, TEntry, TEntryDetail> : PerformanceManager where THeader: unmanaged, IPerformanceHeader where TEntry : unmanaged, IPerformanceEntry where TEntryDetail: unmanaged, IPerformanceDetailEntry + { + /// <summary> + /// The magic used for the <see cref="THeader"/>. + /// </summary> + private const uint MagicPerformanceBuffer = 0x46524550; + + /// <summary> + /// The fixed amount of <see cref="TEntryDetail"/> that can be stored in a frame. + /// </summary> + private const int MaxFrameDetailCount = 100; + + private Memory<byte> _buffer; + private Memory<byte> _historyBuffer; + + private Memory<byte> CurrentBuffer => _buffer.Slice(0, _frameSize); + private Memory<byte> CurrentBufferData => CurrentBuffer.Slice(Unsafe.SizeOf<THeader>()); + + private ref THeader CurrentHeader => ref MemoryMarshal.Cast<byte, THeader>(CurrentBuffer.Span)[0]; + + private Span<TEntry> Entries => MemoryMarshal.Cast<byte, TEntry>(CurrentBufferData.Span.Slice(0, GetEntriesSize())); + private Span<TEntryDetail> EntriesDetail => MemoryMarshal.Cast<byte, TEntryDetail>(CurrentBufferData.Span.Slice(GetEntriesSize(), GetEntriesDetailSize())); + + private int _frameSize; + private int _availableFrameCount; + private int _entryCountPerFrame; + private int _detailTarget; + private int _entryIndex; + private int _entryDetailIndex; + private int _indexHistoryWrite; + private int _indexHistoryRead; + private uint _historyFrameIndex; + + public PerformanceManagerGeneric(Memory<byte> buffer, ref AudioRendererConfiguration parameter) + { + _buffer = buffer; + _frameSize = GetRequiredBufferSizeForPerformanceMetricsPerFrame(ref parameter); + + _entryCountPerFrame = (int)GetEntryCount(ref parameter); + _availableFrameCount = buffer.Length / _frameSize - 1; + + _historyFrameIndex = 0; + + _historyBuffer = _buffer.Slice(_frameSize); + + SetupNewHeader(); + } + + private Span<byte> GetBufferFromIndex(Span<byte> data, int index) + { + return data.Slice(index * _frameSize, _frameSize); + } + + private ref THeader GetHeaderFromBuffer(Span<byte> data, int index) + { + return ref MemoryMarshal.Cast<byte, THeader>(GetBufferFromIndex(data, index))[0]; + } + + private Span<TEntry> GetEntriesFromBuffer(Span<byte> data, int index) + { + return MemoryMarshal.Cast<byte, TEntry>(GetBufferFromIndex(data, index).Slice(Unsafe.SizeOf<THeader>(), GetEntriesSize())); + } + + private Span<TEntryDetail> GetEntriesDetailFromBuffer(Span<byte> data, int index) + { + return MemoryMarshal.Cast<byte, TEntryDetail>(GetBufferFromIndex(data, index).Slice(Unsafe.SizeOf<THeader>() + GetEntriesSize(), GetEntriesDetailSize())); + } + + private void SetupNewHeader() + { + _entryIndex = 0; + _entryDetailIndex = 0; + + CurrentHeader.SetEntryCount(0); + CurrentHeader.SetEntryDetailCount(0); + } + + public static uint GetEntryCount(ref AudioRendererConfiguration parameter) + { + return parameter.VoiceCount + parameter.EffectCount + parameter.SubMixBufferCount + parameter.SinkCount + 1; + } + + public int GetEntriesSize() + { + return Unsafe.SizeOf<TEntry>() * _entryCountPerFrame; + } + + public static int GetEntriesDetailSize() + { + return Unsafe.SizeOf<TEntryDetail>() * MaxFrameDetailCount; + } + + public static int GetRequiredBufferSizeForPerformanceMetricsPerFrame(ref AudioRendererConfiguration parameter) + { + return Unsafe.SizeOf<TEntry>() * (int)GetEntryCount(ref parameter) + GetEntriesDetailSize() + Unsafe.SizeOf<THeader>(); + } + + public override uint CopyHistories(Span<byte> performanceOutput) + { + if (performanceOutput.IsEmpty) + { + return 0; + } + + int nextOffset = 0; + + while (_indexHistoryRead != _indexHistoryWrite) + { + if (nextOffset >= performanceOutput.Length) + { + break; + } + + ref THeader inputHeader = ref GetHeaderFromBuffer(_historyBuffer.Span, _indexHistoryRead); + Span<TEntry> inputEntries = GetEntriesFromBuffer(_historyBuffer.Span, _indexHistoryRead); + Span<TEntryDetail> inputEntriesDetail = GetEntriesDetailFromBuffer(_historyBuffer.Span, _indexHistoryRead); + + Span<byte> targetSpan = performanceOutput.Slice(nextOffset); + + ref THeader outputHeader = ref MemoryMarshal.Cast<byte, THeader>(targetSpan)[0]; + + nextOffset += Unsafe.SizeOf<THeader>(); + + Span<TEntry> outputEntries = MemoryMarshal.Cast<byte, TEntry>(targetSpan.Slice(nextOffset)); + + int totalProcessingTime = 0; + + int effectiveEntryCount = 0; + + for (int entryIndex = 0; entryIndex < inputHeader.GetEntryCount(); entryIndex++) + { + ref TEntry input = ref inputEntries[entryIndex]; + + if (input.GetProcessingTime() != 0 || input.GetStartTime() != 0) + { + ref TEntry output = ref outputEntries[effectiveEntryCount++]; + + output = input; + + nextOffset += Unsafe.SizeOf<TEntry>(); + + totalProcessingTime += input.GetProcessingTime(); + } + } + + Span<TEntryDetail> outputEntriesDetail = MemoryMarshal.Cast<byte, TEntryDetail>(targetSpan.Slice(nextOffset)); + + int effectiveEntryDetailCount = 0; + + for (int entryDetailIndex = 0; entryDetailIndex < inputHeader.GetEntryDetailCount(); entryDetailIndex++) + { + ref TEntryDetail input = ref inputEntriesDetail[entryDetailIndex]; + + if (input.GetProcessingTime() != 0 || input.GetStartTime() != 0) + { + ref TEntryDetail output = ref outputEntriesDetail[effectiveEntryDetailCount++]; + + output = input; + + nextOffset += Unsafe.SizeOf<TEntryDetail>(); + } + } + + outputHeader = inputHeader; + outputHeader.SetMagic(MagicPerformanceBuffer); + outputHeader.SetTotalProcessingTime(totalProcessingTime); + outputHeader.SetNextOffset(nextOffset); + outputHeader.SetEntryCount(effectiveEntryCount); + outputHeader.SetEntryDetailCount(effectiveEntryDetailCount); + + _indexHistoryRead = (_indexHistoryRead + 1) % _availableFrameCount; + } + + if (nextOffset < performanceOutput.Length && (performanceOutput.Length - nextOffset) >= Unsafe.SizeOf<THeader>()) + { + ref THeader outputHeader = ref MemoryMarshal.Cast<byte, THeader>(performanceOutput.Slice(nextOffset))[0]; + + outputHeader = default; + } + + return (uint)nextOffset; + } + + public override bool GetNextEntry(out PerformanceEntryAddresses performanceEntry, PerformanceEntryType entryType, int nodeId) + { + performanceEntry = new PerformanceEntryAddresses(); + performanceEntry.BaseMemory = SpanMemoryManager<int>.Cast(CurrentBuffer); + performanceEntry.EntryCountOffset = (uint)CurrentHeader.GetEntryCountOffset(); + + uint baseEntryOffset = (uint)(Unsafe.SizeOf<THeader>() + Unsafe.SizeOf<TEntry>() * _entryIndex); + + ref TEntry entry = ref Entries[_entryIndex]; + + performanceEntry.StartTimeOffset = baseEntryOffset + (uint)entry.GetStartTimeOffset(); + performanceEntry.ProcessingTimeOffset = baseEntryOffset + (uint)entry.GetProcessingTimeOffset(); + + entry = default; + entry.SetEntryType(entryType); + entry.SetNodeId(nodeId); + + _entryIndex++; + + return true; + } + + public override bool GetNextEntry(out PerformanceEntryAddresses performanceEntry, PerformanceDetailType detailType, PerformanceEntryType entryType, int nodeId) + { + performanceEntry = null; + + if (_entryDetailIndex > MaxFrameDetailCount) + { + return false; + } + + performanceEntry = new PerformanceEntryAddresses(); + performanceEntry.BaseMemory = SpanMemoryManager<int>.Cast(CurrentBuffer); + performanceEntry.EntryCountOffset = (uint)CurrentHeader.GetEntryCountOffset(); + + uint baseEntryOffset = (uint)(Unsafe.SizeOf<THeader>() + GetEntriesSize() + Unsafe.SizeOf<IPerformanceDetailEntry>() * _entryDetailIndex); + + ref TEntryDetail entryDetail = ref EntriesDetail[_entryDetailIndex]; + + performanceEntry.StartTimeOffset = baseEntryOffset + (uint)entryDetail.GetStartTimeOffset(); + performanceEntry.ProcessingTimeOffset = baseEntryOffset + (uint)entryDetail.GetProcessingTimeOffset(); + + entryDetail = default; + entryDetail.SetDetailType(detailType); + entryDetail.SetEntryType(entryType); + entryDetail.SetNodeId(nodeId); + + _entryDetailIndex++; + + return true; + } + + public override bool IsTargetNodeId(int target) + { + return _detailTarget == target; + } + + public override void SetTargetNodeId(int target) + { + _detailTarget = target; + } + + public override void TapFrame(bool dspRunningBehind, uint voiceDropCount, ulong startRenderingTicks) + { + if (_availableFrameCount > 0) + { + int targetIndexForHistory = _indexHistoryWrite; + + _indexHistoryWrite = (_indexHistoryWrite + 1) % _availableFrameCount; + + ref THeader targetHeader = ref GetHeaderFromBuffer(_historyBuffer.Span, targetIndexForHistory); + + CurrentBuffer.Span.CopyTo(GetBufferFromIndex(_historyBuffer.Span, targetIndexForHistory)); + + uint targetHistoryFrameIndex = _historyFrameIndex; + + if (_historyFrameIndex == uint.MaxValue) + { + _historyFrameIndex = 0; + } + else + { + _historyFrameIndex++; + } + + targetHeader.SetDspRunningBehind(dspRunningBehind); + targetHeader.SetVoiceDropCount(voiceDropCount); + targetHeader.SetStartRenderingTicks(startRenderingTicks); + targetHeader.SetIndex(targetHistoryFrameIndex); + + // Finally setup the new header + SetupNewHeader(); + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/RendererSystemContext.cs b/Ryujinx.Audio/Renderer/Server/RendererSystemContext.cs new file mode 100644 index 00000000..a89a8a60 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/RendererSystemContext.cs @@ -0,0 +1,65 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Server.Upsampler; +using System; + +namespace Ryujinx.Audio.Renderer.Server +{ + /// <summary> + /// Represents a lite version of <see cref="AudioRenderSystem"/> used by the <see cref="Dsp.AudioProcessor"/> + /// </summary> + /// <remarks> + /// This also allows to reduce dependencies on the <see cref="AudioRenderSystem"/> for unit testing. + /// </remarks> + public sealed class RendererSystemContext + { + /// <summary> + /// The session id of the current renderer. + /// </summary> + public int SessionId; + + /// <summary> + /// The target channel count for sink. + /// </summary> + /// <remarks>See <see cref="CommandGenerator.GenerateDevice(Sink.DeviceSink, ref Mix.MixState)"/> for usage.</remarks> + public uint ChannelCount; + + /// <summary> + /// The total count of mix buffer. + /// </summary> + public uint MixBufferCount; + + /// <summary> + /// Instance of the <see cref="BehaviourContext"/> used to derive bug fixes and features of the current audio renderer revision. + /// </summary> + public BehaviourContext BehaviourContext; + + /// <summary> + /// Instance of the <see cref="UpsamplerManager"/> used for upsampling (see <see cref="UpsamplerState"/>) + /// </summary> + public UpsamplerManager UpsamplerManager; + + /// <summary> + /// The memory to use for depop processing. + /// </summary> + /// <remarks> + /// See <see cref="Dsp.Command.DepopForMixBuffersCommand"/> and <see cref="Dsp.Command.DepopPrepareCommand"/> + /// </remarks> + public Memory<float> DepopBuffer; + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Sink/BaseSink.cs b/Ryujinx.Audio/Renderer/Server/Sink/BaseSink.cs new file mode 100644 index 00000000..949e3971 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Sink/BaseSink.cs @@ -0,0 +1,119 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Server.MemoryPool; +using System.Diagnostics; +using static Ryujinx.Audio.Renderer.Common.BehaviourParameter; + +namespace Ryujinx.Audio.Renderer.Server.Sink +{ + /// <summary> + /// Base class used for server information of a sink. + /// </summary> + public class BaseSink + { + /// <summary> + /// The type of this <see cref="BaseSink"/>. + /// </summary> + public SinkType Type; + + /// <summary> + /// Set to true if the sink is used. + /// </summary> + public bool IsUsed; + + /// <summary> + /// Set to true if the sink need to be skipped because of invalid state. + /// </summary> + public bool ShouldSkip; + + /// <summary> + /// The node id of the sink. + /// </summary> + public int NodeId; + + /// <summary> + /// Create a new <see cref="BaseSink"/>. + /// </summary> + public BaseSink() + { + CleanUp(); + } + + /// <summary> + /// Clean up the internal state of the <see cref="BaseSink"/>. + /// </summary> + public virtual void CleanUp() + { + Type = TargetSinkType; + IsUsed = false; + ShouldSkip = false; + } + + /// <summary> + /// The target <see cref="SinkType"/> handled by this <see cref="BaseSink"/>. + /// </summary> + public virtual SinkType TargetSinkType => SinkType.Invalid; + + /// <summary> + /// Check if the <see cref="SinkType"/> sent by the user match the internal <see cref="SinkType"/>. + /// </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) + { + return parameter.Type == TargetSinkType; + } + + /// <summary> + /// Update the <see cref="BaseSink"/> state during command generation. + /// </summary> + public virtual void UpdateForCommandGeneration() + { + Debug.Assert(Type == TargetSinkType); + } + + /// <summary> + /// Update the internal common parameters from user parameter. + /// </summary> + /// <param name="parameter">The user parameter.</param> + protected void UpdateStandardParameter(ref SinkInParameter parameter) + { + if (IsUsed != parameter.IsUsed) + { + IsUsed = parameter.IsUsed; + NodeId = parameter.NodeId; + } + } + + /// <summary> + /// Update the internal state from user parameter. + /// </summary> + /// <param name="errorInfo">The possible <see cref="ErrorInfo"/> that was generated.</param> + /// <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) + { + Debug.Assert(IsTypeValid(ref parameter)); + + errorInfo = new ErrorInfo(); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Sink/CircularBufferSink.cs b/Ryujinx.Audio/Renderer/Server/Sink/CircularBufferSink.cs new file mode 100644 index 00000000..ceb955e5 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Sink/CircularBufferSink.cs @@ -0,0 +1,126 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Parameter.Sink; +using Ryujinx.Audio.Renderer.Server.MemoryPool; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Sink +{ + /// <summary> + /// Server information for a circular buffer sink. + /// </summary> + public class CircularBufferSink : BaseSink + { + /// <summary> + /// The circular buffer parameter. + /// </summary> + public CircularBufferParameter Parameter; + + /// <summary> + /// The last written data offset on the circular buffer. + /// </summary> + private uint _lastWrittenOffset; + + /// <summary> + /// THe previous written offset of the circular buffer. + /// </summary> + private uint _oldWrittenOffset; + + /// <summary> + /// The current offset to write data on the circular buffer. + /// </summary> + public uint CurrentWriteOffset { get; private set; } + + /// <summary> + /// The <see cref="AddressInfo"/> of the circular buffer. + /// </summary> + public AddressInfo CircularBufferAddressInfo; + + public CircularBufferSink() + { + CircularBufferAddressInfo = AddressInfo.Create(); + } + + public override SinkType TargetSinkType => SinkType.CircularBuffer; + + public override void Update(out BehaviourParameter.ErrorInfo errorInfo, ref SinkInParameter parameter, ref SinkOutStatus outStatus, PoolMapper mapper) + { + errorInfo = new BehaviourParameter.ErrorInfo(); + outStatus = new SinkOutStatus(); + + Debug.Assert(IsTypeValid(ref parameter)); + + ref CircularBufferParameter inputDeviceParameter = ref MemoryMarshal.Cast<byte, CircularBufferParameter>(parameter.SpecificData)[0]; + + if (parameter.IsUsed != IsUsed || ShouldSkip) + { + UpdateStandardParameter(ref parameter); + + if (parameter.IsUsed) + { + Debug.Assert(CircularBufferAddressInfo.CpuAddress == 0); + Debug.Assert(CircularBufferAddressInfo.GetReference(false) == 0); + + ShouldSkip = !mapper.TryAttachBuffer(out errorInfo, ref CircularBufferAddressInfo, inputDeviceParameter.BufferAddress, inputDeviceParameter.BufferSize); + } + else + { + Debug.Assert(CircularBufferAddressInfo.CpuAddress != 0); + Debug.Assert(CircularBufferAddressInfo.GetReference(false) != 0); + } + + Parameter = inputDeviceParameter; + } + + outStatus.LastWrittenOffset = _lastWrittenOffset; + } + + public override void UpdateForCommandGeneration() + { + Debug.Assert(Type == TargetSinkType); + + if (IsUsed) + { + uint frameSize = Constants.TargetSampleSize * Parameter.SampleCount * Parameter.InputCount; + + _lastWrittenOffset = _oldWrittenOffset; + + _oldWrittenOffset = CurrentWriteOffset; + + CurrentWriteOffset += frameSize; + + if (Parameter.BufferSize > 0) + { + CurrentWriteOffset %= Parameter.BufferSize; + } + } + } + + public override void CleanUp() + { + CircularBufferAddressInfo = AddressInfo.Create(); + _lastWrittenOffset = 0; + _oldWrittenOffset = 0; + CurrentWriteOffset = 0; + base.CleanUp(); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Sink/DeviceSink.cs b/Ryujinx.Audio/Renderer/Server/Sink/DeviceSink.cs new file mode 100644 index 00000000..79af05f7 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Sink/DeviceSink.cs @@ -0,0 +1,92 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Parameter.Sink; +using Ryujinx.Audio.Renderer.Server.MemoryPool; +using Ryujinx.Audio.Renderer.Server.Upsampler; +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Sink +{ + /// <summary> + /// Server information for a device sink. + /// </summary> + public class DeviceSink : BaseSink + { + /// <summary> + /// The downmix coefficients. + /// </summary> + public float[] DownMixCoefficients; + + /// <summary> + /// The device parameters. + /// </summary> + public DeviceParameter Parameter; + + /// <summary> + /// The upsampler instance used by this sink. + /// </summary> + /// <remarks>Null if no upsampling is needed.</remarks> + public UpsamplerState UpsamplerState; + + /// <summary> + /// Create a new <see cref="DeviceSink"/>. + /// </summary> + public DeviceSink() + { + DownMixCoefficients = new float[4]; + } + + public override void CleanUp() + { + UpsamplerState?.Release(); + + UpsamplerState = null; + + base.CleanUp(); + } + + public override SinkType TargetSinkType => SinkType.Device; + + public override void Update(out BehaviourParameter.ErrorInfo errorInfo, ref SinkInParameter parameter, ref SinkOutStatus outStatus, PoolMapper mapper) + { + Debug.Assert(IsTypeValid(ref parameter)); + + ref DeviceParameter inputDeviceParameter = ref MemoryMarshal.Cast<byte, DeviceParameter>(parameter.SpecificData)[0]; + + if (parameter.IsUsed != IsUsed) + { + UpdateStandardParameter(ref parameter); + Parameter = inputDeviceParameter; + } + else + { + Parameter.DownMixParameterEnabled = inputDeviceParameter.DownMixParameterEnabled; + inputDeviceParameter.DownMixParameter.ToSpan().CopyTo(Parameter.DownMixParameter.ToSpan()); + } + + Parameter.DownMixParameter.ToSpan().CopyTo(DownMixCoefficients.AsSpan()); + + errorInfo = new BehaviourParameter.ErrorInfo(); + outStatus = new SinkOutStatus(); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Sink/SinkContext.cs b/Ryujinx.Audio/Renderer/Server/Sink/SinkContext.cs new file mode 100644 index 00000000..b57dbde9 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Sink/SinkContext.cs @@ -0,0 +1,73 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using System.Diagnostics; + +namespace Ryujinx.Audio.Renderer.Server.Sink +{ + /// <summary> + /// Sink context. + /// </summary> + public class SinkContext + { + /// <summary> + /// Storage for <see cref="BaseSink"/>. + /// </summary> + private BaseSink[] _sinks; + + /// <summary> + /// The total sink count. + /// </summary> + private uint _sinkCount; + + /// <summary> + /// Initialize the <see cref="SinkContext"/>. + /// </summary> + /// <param name="sinksCount">The total sink count.</param> + public void Initialize(uint sinksCount) + { + _sinkCount = sinksCount; + _sinks = new BaseSink[_sinkCount]; + + for (int i = 0; i < _sinkCount; i++) + { + _sinks[i] = new BaseSink(); + } + } + + /// <summary> + /// Get the total sink count. + /// </summary> + /// <returns>The total sink count.</returns> + public uint GetCount() + { + return _sinkCount; + } + + /// <summary> + /// Get a reference to a <see cref="BaseSink"/> at the given <paramref name="id"/>. + /// </summary> + /// <param name="id">The index to use.</param> + /// <returns>A reference to a <see cref="BaseSink"/> at the given <paramref name="id"/>.</returns> + public ref BaseSink GetSink(int id) + { + Debug.Assert(id >= 0 && id < _sinkCount); + + return ref _sinks[id]; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Splitter/SplitterContext.cs b/Ryujinx.Audio/Renderer/Server/Splitter/SplitterContext.cs new file mode 100644 index 00000000..0e107a53 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Splitter/SplitterContext.cs @@ -0,0 +1,320 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Utils; +using Ryujinx.Common; +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Splitter +{ + /// <summary> + /// Splitter context. + /// </summary> + public class SplitterContext + { + /// <summary> + /// Storage for <see cref="SplitterState"/>. + /// </summary> + private Memory<SplitterState> _splitters; + + /// <summary> + /// Storage for <see cref="SplitterDestination"/>. + /// </summary> + private Memory<SplitterDestination> _splitterDestinations; + + /// <summary> + /// If set to true, trust the user destination count in <see cref="SplitterState.Update(SplitterContext, ref SplitterInParameter, ReadOnlySpan{byte})"/>. + /// </summary> + public bool IsBugFixed { get; private set; } + + /// <summary> + /// Initialize <see cref="SplitterContext"/>. + /// </summary> + /// <param name="behaviourContext">The behaviour context.</param> + /// <param name="parameter">The audio renderer configuration.</param> + /// <param name="workBufferAllocator">The <see cref="WorkBufferAllocator"/>.</param> + /// <returns>Return true if the initialization was successful.</returns> + public bool Initialize(ref BehaviourContext behaviourContext, ref AudioRendererConfiguration parameter, WorkBufferAllocator workBufferAllocator) + { + if (!behaviourContext.IsSplitterSupported() || parameter.SplitterCount <= 0 || parameter.SplitterDestinationCount <= 0) + { + Setup(Memory<SplitterState>.Empty, Memory<SplitterDestination>.Empty, false); + + return true; + } + + Memory<SplitterState> splitters = workBufferAllocator.Allocate<SplitterState>(parameter.SplitterCount, SplitterState.Alignment); + + if (splitters.IsEmpty) + { + return false; + } + + int splitterId = 0; + + foreach (ref SplitterState splitter in splitters.Span) + { + splitter = new SplitterState(splitterId++); + } + + Memory<SplitterDestination> splitterDestinations = workBufferAllocator.Allocate<SplitterDestination>(parameter.SplitterDestinationCount, + SplitterDestination.Alignment); + + if (splitterDestinations.IsEmpty) + { + return false; + } + + int splitterDestinationId = 0; + foreach (ref SplitterDestination data in splitterDestinations.Span) + { + data = new SplitterDestination(splitterDestinationId++); + } + + SplitterState.InitializeSplitters(splitters.Span); + + Setup(splitters, splitterDestinations, behaviourContext.IsSplitterBugFixed()); + + return true; + } + + /// <summary> + /// Get the work buffer size while adding the size needed for splitter to operate. + /// </summary> + /// <param name="size">The current size.</param> + /// <param name="behaviourContext">The behaviour context.</param> + /// <param name="parameter">The renderer configuration.</param> + /// <returns>Return the new size taking splitter into account.</returns> + public static ulong GetWorkBufferSize(ulong size, ref BehaviourContext behaviourContext, ref AudioRendererConfiguration parameter) + { + if (behaviourContext.IsSplitterSupported()) + { + size = WorkBufferAllocator.GetTargetSize<SplitterState>(size, parameter.SplitterCount, SplitterState.Alignment); + size = WorkBufferAllocator.GetTargetSize<SplitterDestination>(size, parameter.SplitterDestinationCount, SplitterDestination.Alignment); + + if (behaviourContext.IsSplitterBugFixed()) + { + size = WorkBufferAllocator.GetTargetSize<int>(size, parameter.SplitterDestinationCount, 0x10); + } + + return size; + } + else + { + return size; + } + } + + /// <summary> + /// Setup the <see cref="SplitterContext"/> instance. + /// </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> + private void Setup(Memory<SplitterState> splitters, Memory<SplitterDestination> splitterDestinations, bool isBugFixed) + { + _splitters = splitters; + _splitterDestinations = splitterDestinations; + IsBugFixed = isBugFixed; + } + + /// <summary> + /// Clear the new connection flag. + /// </summary> + private void ClearAllNewConnectionFlag() + { + foreach (ref SplitterState splitter in _splitters.Span) + { + splitter.ClearNewConnectionFlag(); + } + } + + /// <summary> + /// Get the destination count using the count of splitter. + /// </summary> + /// <returns>The destination count using the count of splitter.</returns> + public int GetDestinationCountPerStateForCompatibility() + { + if (_splitters.IsEmpty) + { + return 0; + } + + return _splitterDestinations.Length / _splitters.Length; + } + + /// <summary> + /// Update one or multiple <see cref="SplitterState"/> from user parameters. + /// </summary> + /// <param name="inputHeader">The splitter header.</param> + /// <param name="input">The raw data after the splitter header.</param> + private void UpdateState(ref SplitterInParameterHeader inputHeader, ref ReadOnlySpan<byte> input) + { + for (int i = 0; i < inputHeader.SplitterCount; i++) + { + SplitterInParameter parameter = MemoryMarshal.Read<SplitterInParameter>(input); + + 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, ref parameter, input.Slice(Unsafe.SizeOf<SplitterInParameter>())); + } + + input = input.Slice(0x1C + (int)parameter.DestinationCount * 4); + } + } + } + + /// <summary> + /// Update one or multiple <see cref="SplitterDestination"/> from user parameters. + /// </summary> + /// <param name="inputHeader">The splitter header.</param> + /// <param name="input">The raw data after the splitter header.</param> + private void UpdateData(ref SplitterInParameterHeader inputHeader, ref ReadOnlySpan<byte> input) + { + for (int i = 0; i < inputHeader.SplitterDestinationCount; i++) + { + SplitterDestinationInParameter parameter = MemoryMarshal.Read<SplitterDestinationInParameter>(input); + + Debug.Assert(parameter.IsMagicValid()); + + if (parameter.IsMagicValid()) + { + if (parameter.Id >= 0 && parameter.Id < _splitterDestinations.Length) + { + ref SplitterDestination destination = ref GetDestination(parameter.Id); + + destination.Update(parameter); + } + + input = input.Slice(Unsafe.SizeOf<SplitterDestinationInParameter>()); + } + } + } + + /// <summary> + /// 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) + { + if (_splitterDestinations.IsEmpty || _splitters.IsEmpty) + { + consumedSize = 0; + + return true; + } + + int originalSize = input.Length; + + SplitterInParameterHeader header = SpanIOHelper.Read<SplitterInParameterHeader>(ref input); + + if (header.IsMagicValid()) + { + ClearAllNewConnectionFlag(); + + UpdateState(ref header, ref input); + UpdateData(ref header, ref input); + + consumedSize = BitUtils.AlignUp(originalSize - input.Length, 0x10); + + return true; + } + else + { + consumedSize = 0; + + return false; + } + } + + /// <summary> + /// Get a reference to a <see cref="SplitterState"/> at the given <paramref name="id"/>. + /// </summary> + /// <param name="id">The index to use.</param> + /// <returns>A reference to a <see cref="SplitterState"/> at the given <paramref name="id"/>.</returns> + public ref SplitterState GetState(int id) + { + return ref SpanIOHelper.GetFromMemory(_splitters, id, (uint)_splitters.Length); + } + + /// <summary> + /// Get a reference to a <see cref="SplitterDestination"/> at the given <paramref name="id"/>. + /// </summary> + /// <param name="id">The index to use.</param> + /// <returns>A reference to a <see cref="SplitterDestination"/> at the given <paramref name="id"/>.</returns> + public ref SplitterDestination GetDestination(int id) + { + return ref SpanIOHelper.GetFromMemory(_splitterDestinations, id, (uint)_splitterDestinations.Length); + } + + /// <summary> + /// Get a <see cref="Memory{SplitterDestination}"/> at the given <paramref name="id"/>. + /// </summary> + /// <param name="id">The index to use.</param> + /// <returns>A <see cref="Memory{SplitterDestination}"/> at the given <paramref name="id"/>.</returns> + public Memory<SplitterDestination> GetDestinationMemory(int id) + { + return SpanIOHelper.GetMemory(_splitterDestinations, id, (uint)_splitterDestinations.Length); + } + + /// <summary> + /// Get a <see cref="Span{SplitterDestination}"/> in the <see cref="SplitterState"/> at <paramref name="id"/> and pass <paramref name="destinationId"/> to <see cref="SplitterState.GetData(int)"/>. + /// </summary> + /// <param name="id">The index to use to get the <see cref="SplitterState"/>.</param> + /// <param name="destinationId">The index of the <see cref="SplitterDestination"/>.</param> + /// <returns>A <see cref="Span{SplitterDestination}"/>.</returns> + public Span<SplitterDestination> GetDestination(int id, int destinationId) + { + ref SplitterState splitter = ref GetState(id); + + return splitter.GetData(destinationId); + } + + /// <summary> + /// Return true if the audio renderer has any splitters. + /// </summary> + /// <returns>True if the audio renderer has any splitters.</returns> + public bool UsingSplitter() + { + return !_splitters.IsEmpty && !_splitterDestinations.IsEmpty; + } + + /// <summary> + /// Update the internal state of all splitters. + /// </summary> + public void UpdateInternalState() + { + foreach (ref SplitterState splitter in _splitters.Span) + { + splitter.UpdateInternalState(); + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestination.cs b/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestination.cs new file mode 100644 index 00000000..077bf8ae --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestination.cs @@ -0,0 +1,210 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Common.Utilities; +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Splitter +{ + /// <summary> + /// Server state for a splitter destination. + /// </summary> + [StructLayout(LayoutKind.Sequential, Size = 0xE0, Pack = Alignment)] + public struct SplitterDestination + { + public const int Alignment = 0x10; + + /// <summary> + /// The unique id of this <see cref="SplitterDestination"/>. + /// </summary> + public int Id; + + /// <summary> + /// The mix to output the result of the splitter. + /// </summary> + public int DestinationId; + + /// <summary> + /// Mix buffer volumes storage. + /// </summary> + private MixArray _mix; + private MixArray _previousMix; + + /// <summary> + /// Pointer to the next linked element. + /// </summary> + private unsafe SplitterDestination* _next; + + /// <summary> + /// Set to true if in use. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool IsUsed; + + /// <summary> + /// Set to true if the internal state need to be updated. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool NeedToUpdateInternalState; + + [StructLayout(LayoutKind.Sequential, Size = 4 * Constants.MixBufferCountMax, Pack = 1)] + private struct MixArray { } + + /// <summary> + /// Mix buffer volumes. + /// </summary> + /// <remarks>Used when a splitter id is specified in the mix.</remarks> + public Span<float> MixBufferVolume => SpanHelpers.AsSpan<MixArray, float>(ref _mix); + + /// <summary> + /// Previous mix buffer volumes. + /// </summary> + /// <remarks>Used when a splitter id is specified in the mix.</remarks> + public Span<float> PreviousMixBufferVolume => SpanHelpers.AsSpan<MixArray, float>(ref _previousMix); + + /// <summary> + /// Get the <see cref="Span{SplitterDestination}"/> of the next element or <see cref="Span{SplitterDestination}.Empty"/> if not present. + /// </summary> + public Span<SplitterDestination> Next + { + get + { + unsafe + { + return _next != null ? new Span<SplitterDestination>(_next, 1) : Span<SplitterDestination>.Empty; + } + } + } + + /// <summary> + /// Create a new <see cref="SplitterDestination"/>. + /// </summary> + /// <param name="id">The unique id of this <see cref="SplitterDestination"/>.</param> + public SplitterDestination(int id) : this() + { + Id = id; + DestinationId = Constants.UnusedMixId; + + ClearVolumes(); + } + + /// <summary> + /// Update the <see cref="SplitterDestination"/> from user parameter. + /// </summary> + /// <param name="parameter">The user parameter.</param> + public void Update(SplitterDestinationInParameter parameter) + { + Debug.Assert(Id == parameter.Id); + + if (parameter.IsMagicValid() && Id == parameter.Id) + { + DestinationId = parameter.DestinationId; + + parameter.MixBufferVolume.CopyTo(MixBufferVolume); + + if (!IsUsed && parameter.IsUsed) + { + MixBufferVolume.CopyTo(PreviousMixBufferVolume); + + NeedToUpdateInternalState = false; + } + + IsUsed = parameter.IsUsed; + } + } + + /// <summary> + /// Update the internal state of the instance. + /// </summary> + public void UpdateInternalState() + { + if (IsUsed && NeedToUpdateInternalState) + { + MixBufferVolume.CopyTo(PreviousMixBufferVolume); + } + + NeedToUpdateInternalState = false; + } + + /// <summary> + /// Set the update internal state marker. + /// </summary> + public void MarkAsNeedToUpdateInternalState() + { + NeedToUpdateInternalState = true; + } + + /// <summary> + /// Return true if the <see cref="SplitterDestination"/> is used and has a destination. + /// </summary> + /// <returns>True if the <see cref="SplitterDestination"/> is used and has a destination.</returns> + public bool IsConfigured() + { + return IsUsed && DestinationId != Constants.UnusedMixId; + } + + /// <summary> + /// Get the volume for a given destination. + /// </summary> + /// <param name="destinationIndex">The destination index to use.</param> + /// <returns>The volume for the given destination.</returns> + public float GetMixVolume(int destinationIndex) + { + Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax); + + return MixBufferVolume[destinationIndex]; + } + + /// <summary> + /// Clear the volumes. + /// </summary> + public void ClearVolumes() + { + MixBufferVolume.Fill(0); + PreviousMixBufferVolume.Fill(0); + } + + /// <summary> + /// Link the next element to the given <see cref="SplitterDestination"/>. + /// </summary> + /// <param name="next">The given <see cref="SplitterDestination"/> to link.</param> + public void Link(ref SplitterDestination next) + { + unsafe + { + fixed (SplitterDestination *nextPtr = &next) + { + _next = nextPtr; + } + } + } + + /// <summary> + /// Remove the link to the next element. + /// </summary> + public void Unlink() + { + unsafe + { + _next = null; + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Splitter/SplitterState.cs b/Ryujinx.Audio/Renderer/Server/Splitter/SplitterState.cs new file mode 100644 index 00000000..c928ec53 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Splitter/SplitterState.cs @@ -0,0 +1,237 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// +using Ryujinx.Audio.Renderer.Parameter; +using System; +using System.Buffers; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Splitter +{ + /// <summary> + /// Server state for a splitter. + /// </summary> + [StructLayout(LayoutKind.Sequential, Size = 0x20, Pack = Alignment)] + public struct SplitterState + { + public const int Alignment = 0x10; + + /// <summary> + /// The unique id of this <see cref="SplitterState"/>. + /// </summary> + public int Id; + + /// <summary> + /// Target sample rate to use on the splitter. + /// </summary> + public uint SampleRate; + + /// <summary> + /// Count of splitter destinations (<see cref="SplitterDestination"/>). + /// </summary> + public int DestinationCount; + + /// <summary> + /// Set to true if the splitter has a new connection. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool HasNewConnection; + + /// <summary> + /// Linked list of <see cref="SplitterDestination"/>. + /// </summary> + private unsafe SplitterDestination* _destinationsData; + + /// <summary> + /// Span to the first element of the linked list of <see cref="SplitterDestination"/>. + /// </summary> + public Span<SplitterDestination> Destinations + { + get + { + unsafe + { + return (IntPtr)_destinationsData != IntPtr.Zero ? new Span<SplitterDestination>(_destinationsData, 1) : Span<SplitterDestination>.Empty; + } + } + } + + /// <summary> + /// Create a new <see cref="SplitterState"/>. + /// </summary> + /// <param name="id">The unique id of this <see cref="SplitterState"/>.</param> + public SplitterState(int id) : this() + { + Id = id; + } + + public Span<SplitterDestination> GetData(int index) + { + int i = 0; + + Span<SplitterDestination> result = Destinations; + + while (i < index) + { + if (result.IsEmpty) + { + break; + } + + result = result[0].Next; + i++; + } + + return result; + } + + /// <summary> + /// Clear the new connection flag. + /// </summary> + public void ClearNewConnectionFlag() + { + HasNewConnection = false; + } + + /// <summary> + /// Utility function to apply a given <see cref="SpanAction{T, TArg}"/> to all <see cref="Destinations"/>. + /// </summary> + /// <param name="action">The action to execute on each elements.</param> + private void ForEachDestination(SpanAction<SplitterDestination, int> action) + { + Span<SplitterDestination> temp = Destinations; + + int i = 0; + + while (true) + { + if (temp.IsEmpty) + { + break; + } + + Span<SplitterDestination> next = temp[0].Next; + + action.Invoke(temp, i++); + + temp = next; + } + } + + /// <summary> + /// Update the <see cref="SplitterState"/> from user parameter. + /// </summary> + /// <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) + { + ClearLinks(); + + int destinationCount; + + if (context.IsBugFixed) + { + destinationCount = parameter.DestinationCount; + } + else + { + destinationCount = Math.Min(context.GetDestinationCountPerStateForCompatibility(), parameter.DestinationCount); + } + + if (destinationCount > 0) + { + ReadOnlySpan<int> destinationIds = MemoryMarshal.Cast<byte, int>(input); + + Memory<SplitterDestination> destination = context.GetDestinationMemory(destinationIds[0]); + + SetDestination(ref destination.Span[0]); + + DestinationCount = destinationCount; + + for (int i = 1; i < destinationCount; i++) + { + Memory<SplitterDestination> nextDestination = context.GetDestinationMemory(destinationIds[i]); + + destination.Span[0].Link(ref nextDestination.Span[0]); + destination = nextDestination; + } + } + + Debug.Assert(parameter.Id == Id); + + if (parameter.Id == Id) + { + SampleRate = parameter.SampleRate; + HasNewConnection = true; + } + } + + /// <summary> + /// Set the head of the linked list of <see cref="Destinations"/>. + /// </summary> + /// <param name="newValue">A reference to a <see cref="SplitterDestination"/>.</param> + public void SetDestination(ref SplitterDestination newValue) + { + unsafe + { + fixed (SplitterDestination* newValuePtr = &newValue) + { + _destinationsData = newValuePtr; + } + } + } + + /// <summary> + /// Update the internal state of this instance. + /// </summary> + public void UpdateInternalState() + { + ForEachDestination((destination, _) => destination[0].UpdateInternalState()); + } + + /// <summary> + /// Clear all links from the <see cref="Destinations"/>. + /// </summary> + public void ClearLinks() + { + ForEachDestination((destination, _) => destination[0].Unlink()); + + unsafe + { + _destinationsData = (SplitterDestination*)IntPtr.Zero; + } + } + + /// <summary> + /// Initialize a given <see cref="Span{SplitterState}"/>. + /// </summary> + /// <param name="splitters">All the <see cref="SplitterState"/> to initialize.</param> + public static void InitializeSplitters(Span<SplitterState> splitters) + { + foreach (ref SplitterState splitter in splitters) + { + unsafe + { + splitter._destinationsData = (SplitterDestination*)IntPtr.Zero; + } + + splitter.DestinationCount = 0; + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/StateUpdater.cs b/Ryujinx.Audio/Renderer/Server/StateUpdater.cs new file mode 100644 index 00000000..77935b75 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/StateUpdater.cs @@ -0,0 +1,575 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Parameter.Performance; +using Ryujinx.Audio.Renderer.Server.Effect; +using Ryujinx.Audio.Renderer.Server.MemoryPool; +using Ryujinx.Audio.Renderer.Server.Mix; +using Ryujinx.Audio.Renderer.Server.Performance; +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.Logging; +using System; +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 + { + private readonly ReadOnlyMemory<byte> _inputOrigin; + private ReadOnlyMemory <byte> _outputOrigin; + private ReadOnlyMemory<byte> _input; + + private Memory<byte> _output; + private uint _processHandle; + private BehaviourContext _behaviourContext; + + private UpdateDataHeader _inputHeader; + private Memory<UpdateDataHeader> _outputHeader; + + private ref UpdateDataHeader OutputHeader => ref _outputHeader.Span[0]; + + public StateUpdater(ReadOnlyMemory<byte> input, Memory<byte> output, uint processHandle, BehaviourContext behaviourContext) + { + _input = input; + _inputOrigin = _input; + _output = output; + _outputOrigin = _output; + _processHandle = processHandle; + _behaviourContext = behaviourContext; + + _inputHeader = SpanIOHelper.Read<UpdateDataHeader>(ref _input); + + _outputHeader = SpanMemoryManager<UpdateDataHeader>.Cast(_output.Slice(0, Unsafe.SizeOf<UpdateDataHeader>())); + OutputHeader.Initialize(_behaviourContext.UserRevision); + _output = _output.Slice(Unsafe.SizeOf<UpdateDataHeader>()); + } + + public ResultCode UpdateBehaviourContext() + { + BehaviourParameter parameter = SpanIOHelper.Read<BehaviourParameter>(ref _input); + + if (!BehaviourContext.CheckValidRevision(parameter.UserRevision) || parameter.UserRevision != _behaviourContext.UserRevision) + { + return ResultCode.InvalidUpdateInfo; + } + + _behaviourContext.ClearError(); + _behaviourContext.UpdateFlags(parameter.Flags); + + if (_inputHeader.BehaviourSize != Unsafe.SizeOf<BehaviourParameter>()) + { + return ResultCode.InvalidUpdateInfo; + } + + return ResultCode.Success; + } + + public ResultCode UpdateMemoryPools(Span<MemoryPoolState> memoryPools) + { + PoolMapper mapper = new PoolMapper(_processHandle, _behaviourContext.IsMemoryPoolForceMappingEnabled()); + + if (memoryPools.Length * Unsafe.SizeOf<MemoryPoolInParameter>() != _inputHeader.MemoryPoolsSize) + { + return ResultCode.InvalidUpdateInfo; + } + + foreach (ref MemoryPoolState memoryPool in memoryPools) + { + MemoryPoolInParameter parameter = SpanIOHelper.Read<MemoryPoolInParameter>(ref _input); + + ref MemoryPoolOutStatus outStatus = ref SpanIOHelper.GetWriteRef<MemoryPoolOutStatus>(ref _output)[0]; + + PoolMapper.UpdateResult updateResult = mapper.Update(ref memoryPool, ref parameter, ref outStatus); + + if (updateResult != PoolMapper.UpdateResult.Success && + updateResult != PoolMapper.UpdateResult.MapError && + updateResult != PoolMapper.UpdateResult.UnmapError) + { + if (updateResult != PoolMapper.UpdateResult.InvalidParameter) + { + throw new InvalidOperationException($"{updateResult}"); + } + + return ResultCode.InvalidUpdateInfo; + } + } + + OutputHeader.MemoryPoolsSize = (uint)(Unsafe.SizeOf<MemoryPoolOutStatus>() * memoryPools.Length); + OutputHeader.TotalSize += OutputHeader.MemoryPoolsSize; + + return ResultCode.Success; + } + + public ResultCode UpdateVoiceChannelResources(VoiceContext context) + { + if (context.GetCount() * Unsafe.SizeOf<VoiceChannelResourceInParameter>() != _inputHeader.VoiceResourcesSize) + { + return ResultCode.InvalidUpdateInfo; + } + + for (int i = 0; i < context.GetCount(); i++) + { + VoiceChannelResourceInParameter parameter = SpanIOHelper.Read<VoiceChannelResourceInParameter>(ref _input); + + ref VoiceChannelResource resource = ref context.GetChannelResource(i); + + resource.Id = parameter.Id; + parameter.Mix.ToSpan().CopyTo(resource.Mix.ToSpan()); + resource.IsUsed = parameter.IsUsed; + } + + return ResultCode.Success; + } + + public ResultCode UpdateVoices(VoiceContext context, Memory<MemoryPoolState> memoryPools) + { + if (context.GetCount() * Unsafe.SizeOf<VoiceInParameter>() != _inputHeader.VoicesSize) + { + return ResultCode.InvalidUpdateInfo; + } + + int initialOutputSize = _output.Length; + + ReadOnlySpan<VoiceInParameter> parameters = MemoryMarshal.Cast<byte, VoiceInParameter>(_input.Slice(0, (int)_inputHeader.VoicesSize).Span); + + _input = _input.Slice((int)_inputHeader.VoicesSize); + + PoolMapper mapper = new PoolMapper(_processHandle, memoryPools, _behaviourContext.IsMemoryPoolForceMappingEnabled()); + + // First make everything not in use. + for (int i = 0; i < context.GetCount(); i++) + { + ref VoiceState state = ref context.GetState(i); + + state.InUse = false; + } + + // Start processing + for (int i = 0; i < context.GetCount(); i++) + { + VoiceInParameter parameter = parameters[i]; + + Memory<VoiceUpdateState>[] voiceUpdateStates = new Memory<VoiceUpdateState>[Constants.VoiceChannelCountMax]; + + ref VoiceOutStatus outStatus = ref SpanIOHelper.GetWriteRef<VoiceOutStatus>(ref _output)[0]; + + if (parameter.InUse) + { + ref VoiceState currentVoiceState = ref context.GetState(i); + + for (int channelResourceIndex = 0; channelResourceIndex < parameter.ChannelCount; channelResourceIndex++) + { + int channelId = parameter.ChannelResourceIds[channelResourceIndex]; + + Debug.Assert(channelId >= 0 && channelId < context.GetCount()); + + voiceUpdateStates[channelResourceIndex] = context.GetUpdateStateForCpu(channelId); + } + + if (parameter.IsNew) + { + currentVoiceState.Initialize(); + } + + currentVoiceState.UpdateParameters(out ErrorInfo updateParameterError, ref parameter, ref mapper, ref _behaviourContext); + + if (updateParameterError.ErrorCode != ResultCode.Success) + { + _behaviourContext.AppendError(ref updateParameterError); + } + + currentVoiceState.UpdateWaveBuffers(out ErrorInfo[] waveBufferUpdateErrorInfos, ref parameter, voiceUpdateStates, ref mapper, ref _behaviourContext); + + foreach (ref ErrorInfo errorInfo in waveBufferUpdateErrorInfos.AsSpan()) + { + if (errorInfo.ErrorCode != ResultCode.Success) + { + _behaviourContext.AppendError(ref errorInfo); + } + } + + currentVoiceState.WriteOutStatus(ref outStatus, ref parameter, voiceUpdateStates); + } + } + + int currentOutputSize = _output.Length; + + OutputHeader.VoicesSize = (uint)(Unsafe.SizeOf<VoiceOutStatus>() * context.GetCount()); + OutputHeader.TotalSize += OutputHeader.VoicesSize; + + Debug.Assert((initialOutputSize - currentOutputSize) == OutputHeader.VoicesSize); + + return ResultCode.Success; + } + + private static void ResetEffect(ref BaseEffect effect, ref EffectInParameter parameter, PoolMapper mapper) + { + effect.ForceUnmapBuffers(mapper); + + switch (parameter.Type) + { + case EffectType.Invalid: + effect = new BaseEffect(); + break; + case EffectType.BufferMix: + effect = new BufferMixEffect(); + break; + case EffectType.AuxiliaryBuffer: + effect = new AuxiliaryBufferEffect(); + break; + case EffectType.Delay: + effect = new DelayEffect(); + break; + case EffectType.Reverb: + effect = new ReverbEffect(); + break; + case EffectType.Reverb3d: + effect = new Reverb3dEffect(); + break; + case EffectType.BiquadFilter: + effect = new BiquadFilterEffect(); + break; + default: + throw new NotImplementedException($"EffectType {parameter.Type} not implemented!"); + } + } + + public ResultCode UpdateEffects(EffectContext context, bool isAudioRendererActive, Memory<MemoryPoolState> memoryPools) + { + if (context.GetCount() * Unsafe.SizeOf<EffectInParameter>() != _inputHeader.EffectsSize) + { + return ResultCode.InvalidUpdateInfo; + } + + int initialOutputSize = _output.Length; + + ReadOnlySpan<EffectInParameter> parameters = MemoryMarshal.Cast<byte, EffectInParameter>(_input.Slice(0, (int)_inputHeader.EffectsSize).Span); + + _input = _input.Slice((int)_inputHeader.EffectsSize); + + PoolMapper mapper = new PoolMapper(_processHandle, memoryPools, _behaviourContext.IsMemoryPoolForceMappingEnabled()); + + for (int i = 0; i < context.GetCount(); i++) + { + EffectInParameter parameter = parameters[i]; + + ref EffectOutStatus outStatus = ref SpanIOHelper.GetWriteRef<EffectOutStatus>(ref _output)[0]; + + ref BaseEffect effect = ref context.GetEffect(i); + + if (!effect.IsTypeValid(ref parameter)) + { + ResetEffect(ref effect, ref parameter, mapper); + } + + effect.Update(out ErrorInfo updateErrorInfo, ref parameter, mapper); + + if (updateErrorInfo.ErrorCode != ResultCode.Success) + { + _behaviourContext.AppendError(ref updateErrorInfo); + } + + effect.StoreStatus(ref outStatus, isAudioRendererActive); + } + + int currentOutputSize = _output.Length; + + OutputHeader.EffectsSize = (uint)(Unsafe.SizeOf<EffectOutStatus>() * context.GetCount()); + OutputHeader.TotalSize += OutputHeader.EffectsSize; + + Debug.Assert((initialOutputSize - currentOutputSize) == OutputHeader.EffectsSize); + + return ResultCode.Success; + } + + public ResultCode UpdateSplitter(SplitterContext context) + { + if (context.Update(_input.Span, out int consumedSize)) + { + _input = _input.Slice(consumedSize); + + return ResultCode.Success; + } + else + { + return ResultCode.InvalidUpdateInfo; + } + } + + private bool CheckMixParametersValidity(MixContext mixContext, uint mixBufferCount, uint inputMixCount, ReadOnlySpan<MixParameter> parameters) + { + uint maxMixStateCount = mixContext.GetCount(); + uint totalRequiredMixBufferCount = 0; + + for (int i = 0; i < inputMixCount; i++) + { + if (parameters[i].IsUsed) + { + if (parameters[i].DestinationMixId != Constants.UnusedMixId && + parameters[i].DestinationMixId > maxMixStateCount && + parameters[i].MixId != Constants.FinalMixId) + { + return true; + } + + totalRequiredMixBufferCount += parameters[i].BufferCount; + } + } + + return totalRequiredMixBufferCount > mixBufferCount; + } + + public ResultCode UpdateMixes(MixContext mixContext, uint mixBufferCount, EffectContext effectContext, SplitterContext splitterContext) + { + uint mixCount; + uint inputMixSize; + uint inputSize = 0; + + if (_behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()) + { + MixInParameterDirtyOnlyUpdate parameter = MemoryMarshal.Cast<byte, MixInParameterDirtyOnlyUpdate>(_input.Span)[0]; + + mixCount = parameter.MixCount; + + inputSize += (uint)Unsafe.SizeOf<MixInParameterDirtyOnlyUpdate>(); + } + else + { + mixCount = mixContext.GetCount(); + } + + inputMixSize = mixCount * (uint)Unsafe.SizeOf<MixParameter>(); + + inputSize += inputMixSize; + + if (inputSize != _inputHeader.MixesSize) + { + return ResultCode.InvalidUpdateInfo; + } + + if (_behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()) + { + _input = _input.Slice(Unsafe.SizeOf<MixInParameterDirtyOnlyUpdate>()); + } + + ReadOnlySpan<MixParameter> parameters = MemoryMarshal.Cast<byte, MixParameter>(_input.Span.Slice(0, (int)inputMixSize)); + + _input = _input.Slice((int)inputMixSize); + + if (CheckMixParametersValidity(mixContext, mixBufferCount, mixCount, parameters)) + { + return ResultCode.InvalidUpdateInfo; + } + + bool isMixContextDirty = false; + + for (int i = 0; i < parameters.Length; i++) + { + MixParameter parameter = parameters[i]; + + int mixId = i; + + if (_behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()) + { + mixId = parameter.MixId; + } + + ref MixState mix = ref mixContext.GetState(mixId); + + if (parameter.IsUsed != mix.IsUsed) + { + mix.IsUsed = parameter.IsUsed; + + if (parameter.IsUsed) + { + mix.ClearEffectProcessingOrder(); + } + + isMixContextDirty = true; + } + + if (mix.IsUsed) + { + isMixContextDirty |= mix.Update(mixContext.EdgeMatrix, ref parameter, effectContext, splitterContext, _behaviourContext); + } + } + + if (isMixContextDirty) + { + if (_behaviourContext.IsSplitterSupported() && splitterContext.UsingSplitter()) + { + if (!mixContext.Sort(splitterContext)) + { + return ResultCode.InvalidMixSorting; + } + } + else + { + mixContext.Sort(); + } + } + + return ResultCode.Success; + } + + private static void ResetSink(ref BaseSink sink, ref SinkInParameter parameter) + { + sink.CleanUp(); + + switch (parameter.Type) + { + case SinkType.Invalid: + sink = new BaseSink(); + break; + case SinkType.CircularBuffer: + sink = new CircularBufferSink(); + break; + case SinkType.Device: + sink = new DeviceSink(); + break; + default: + throw new NotImplementedException($"SinkType {parameter.Type} not implemented!"); + } + } + + public ResultCode UpdateSinks(SinkContext context, Memory<MemoryPoolState> memoryPools) + { + PoolMapper mapper = new PoolMapper(_processHandle, memoryPools, _behaviourContext.IsMemoryPoolForceMappingEnabled()); + + if (context.GetCount() * Unsafe.SizeOf<SinkInParameter>() != _inputHeader.SinksSize) + { + return ResultCode.InvalidUpdateInfo; + } + + int initialOutputSize = _output.Length; + + ReadOnlySpan<SinkInParameter> parameters = MemoryMarshal.Cast<byte, SinkInParameter>(_input.Slice(0, (int)_inputHeader.SinksSize).Span); + + _input = _input.Slice((int)_inputHeader.SinksSize); + + for (int i = 0; i < context.GetCount(); i++) + { + SinkInParameter parameter = parameters[i]; + ref SinkOutStatus outStatus = ref SpanIOHelper.GetWriteRef<SinkOutStatus>(ref _output)[0]; + ref BaseSink sink = ref context.GetSink(i); + + if (!sink.IsTypeValid(ref parameter)) + { + ResetSink(ref sink, ref parameter); + } + + sink.Update(out ErrorInfo updateErrorInfo, ref parameter, ref outStatus, mapper); + + if (updateErrorInfo.ErrorCode != ResultCode.Success) + { + _behaviourContext.AppendError(ref updateErrorInfo); + } + } + + int currentOutputSize = _output.Length; + + OutputHeader.SinksSize = (uint)(Unsafe.SizeOf<SinkOutStatus>() * context.GetCount()); + OutputHeader.TotalSize += OutputHeader.SinksSize; + + Debug.Assert((initialOutputSize - currentOutputSize) == OutputHeader.SinksSize); + + return ResultCode.Success; + } + + public ResultCode UpdatePerformanceBuffer(PerformanceManager manager, Span<byte> performanceOutput) + { + if (Unsafe.SizeOf<PerformanceInParameter>() != _inputHeader.PerformanceBufferSize) + { + return ResultCode.InvalidUpdateInfo; + } + + PerformanceInParameter parameter = SpanIOHelper.Read<PerformanceInParameter>(ref _input); + + ref PerformanceOutStatus outStatus = ref SpanIOHelper.GetWriteRef<PerformanceOutStatus>(ref _output)[0]; + + if (manager != null) + { + outStatus.HistorySize = manager.CopyHistories(performanceOutput); + + manager.SetTargetNodeId(parameter.TargetNodeId); + } + else + { + outStatus.HistorySize = 0; + } + + OutputHeader.PerformanceBufferSize = (uint)Unsafe.SizeOf<PerformanceOutStatus>(); + OutputHeader.TotalSize += OutputHeader.PerformanceBufferSize; + + return ResultCode.Success; + } + + public ResultCode UpdateErrorInfo() + { + ref BehaviourErrorInfoOutStatus outStatus = ref SpanIOHelper.GetWriteRef<BehaviourErrorInfoOutStatus>(ref _output)[0]; + + _behaviourContext.CopyErrorInfo(outStatus.ErrorInfos.ToSpan(), out outStatus.ErrorInfosCount); + + OutputHeader.BehaviourSize = (uint)Unsafe.SizeOf<BehaviourErrorInfoOutStatus>(); + OutputHeader.TotalSize += OutputHeader.BehaviourSize; + + return ResultCode.Success; + } + + public ResultCode UpdateRendererInfo(ulong elapsedFrameCount) + { + ref RendererInfoOutStatus outStatus = ref SpanIOHelper.GetWriteRef<RendererInfoOutStatus>(ref _output)[0]; + + outStatus.ElapsedFrameCount = elapsedFrameCount; + + OutputHeader.RenderInfoSize = (uint)Unsafe.SizeOf<RendererInfoOutStatus>(); + OutputHeader.TotalSize += OutputHeader.RenderInfoSize; + + return ResultCode.Success; + } + + public ResultCode CheckConsumedSize() + { + int consumedInputSize = _inputOrigin.Length - _input.Length; + int consumedOutputSize = _outputOrigin.Length - _output.Length; + + if (consumedInputSize != _inputHeader.TotalSize) + { + Logger.Error?.Print(LogClass.AudioRenderer, $"Consumed input size mismatch (got {consumedInputSize} expected {_inputHeader.TotalSize})"); + + return ResultCode.InvalidUpdateInfo; + } + + if (consumedOutputSize != OutputHeader.TotalSize) + { + Logger.Error?.Print(LogClass.AudioRenderer, $"Consumed output size mismatch (got {consumedOutputSize} expected {OutputHeader.TotalSize})"); + + return ResultCode.InvalidUpdateInfo; + } + + return ResultCode.Success; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Types/AudioRendererExecutionMode.cs b/Ryujinx.Audio/Renderer/Server/Types/AudioRendererExecutionMode.cs new file mode 100644 index 00000000..7bb0507c --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Types/AudioRendererExecutionMode.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +namespace Ryujinx.Audio.Renderer.Server.Types +{ + /// <summary> + /// The execution mode of an <see cref="AudioRenderSystem"/>. + /// </summary> + public enum AudioRendererExecutionMode : byte + { + /// <summary> + /// Automatically send commands to the DSP at a fixed rate (see <see cref="AudioRenderSystem.SendCommands"/> + /// </summary> + Auto, + + /// <summary> + /// Audio renderer operation needs to be done manually via ExecuteAudioRenderer. + /// </summary> + /// <remarks>This is not supported on the DSP and is as such stubbed.</remarks> + Manual + } +}
\ No newline at end of file diff --git a/Ryujinx.Audio/Renderer/Server/Types/AudioRendererRenderingDevice.cs b/Ryujinx.Audio/Renderer/Server/Types/AudioRendererRenderingDevice.cs new file mode 100644 index 00000000..f89086f6 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Types/AudioRendererRenderingDevice.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +namespace Ryujinx.Audio.Renderer.Server.Types +{ + /// <summary> + /// The rendering device of an <see cref="AudioRenderSystem"/>. + /// </summary> + public enum AudioRendererRenderingDevice : byte + { + /// <summary> + /// Rendering is performed on the DSP. + /// </summary> + /// <remarks> + /// Only supports <see cref="AudioRendererExecutionMode.Auto"/>. + /// </remarks> + Dsp, + + /// <summary> + /// Rendering is performed on the CPU. + /// </summary> + /// <remarks> + /// Only supports <see cref="AudioRendererExecutionMode.Manual"/>. + /// </remarks> + Cpu + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Types/PlayState.cs b/Ryujinx.Audio/Renderer/Server/Types/PlayState.cs new file mode 100644 index 00000000..7f8895c9 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Types/PlayState.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +namespace Ryujinx.Audio.Renderer.Server.Types +{ + /// <summary> + /// The internal play state of a <see cref="Voice.VoiceState"/> + /// </summary> + public enum PlayState + { + /// <summary> + /// The voice has been started and is playing. + /// </summary> + Started, + + /// <summary> + /// The voice has been stopped. + /// </summary> + /// <remarks> + /// This cannot be directly set by user. + /// See <see cref="Stopping"/> for correct usage. + /// </remarks> + Stopped, + + /// <summary> + /// The user asked the voice to be stopped. + /// </summary> + /// <remarks> + /// This is changed to the <see cref="Stopped"/> state after command generation. + /// <seealso cref="Voice.VoiceState.UpdateForCommandGeneration(Voice.VoiceContext)"/> + /// </remarks> + Stopping, + + /// <summary> + /// The voice has been paused by user request. + /// </summary> + /// <remarks> + /// The user can resume to the <see cref="Started"/> state. + /// </remarks> + Paused + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Upsampler/UpsamplerManager.cs b/Ryujinx.Audio/Renderer/Server/Upsampler/UpsamplerManager.cs new file mode 100644 index 00000000..6878123b --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Upsampler/UpsamplerManager.cs @@ -0,0 +1,101 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using System; +using System.Diagnostics; + +namespace Ryujinx.Audio.Renderer.Server.Upsampler +{ + /// <summary> + /// Upsampler manager. + /// </summary> + public class UpsamplerManager + { + /// <summary> + /// Work buffer for upsampler. + /// </summary> + private Memory<float> _upSamplerWorkBuffer; + + /// <summary> + /// Global lock of the object. + /// </summary> + private object Lock = new object(); + + /// <summary> + /// The upsamplers instances. + /// </summary> + private UpsamplerState[] _upsamplers; + + /// <summary> + /// The count of upsamplers. + /// </summary> + private uint _count; + + /// <summary> + /// Create a new <see cref="UpsamplerManager"/>. + /// </summary> + /// <param name="upSamplerWorkBuffer">Work buffer for upsampler.</param> + /// <param name="count">The count of upsamplers.</param> + public UpsamplerManager(Memory<float> upSamplerWorkBuffer, uint count) + { + _upSamplerWorkBuffer = upSamplerWorkBuffer; + _count = count; + + _upsamplers = new UpsamplerState[_count]; + } + + /// <summary> + /// Allocate a new <see cref="UpsamplerState"/>. + /// </summary> + /// <returns>A new <see cref="UpsamplerState"/> or null if out of memory.</returns> + public UpsamplerState Allocate() + { + int workBufferOffset = 0; + + lock (Lock) + { + for (int i = 0; i < _count; i++) + { + if (_upsamplers[i] == null) + { + _upsamplers[i] = new UpsamplerState(this, i, _upSamplerWorkBuffer.Slice(workBufferOffset, Constants.UpSampleEntrySize), Constants.TargetSampleCount); + + return _upsamplers[i]; + } + + workBufferOffset += Constants.UpSampleEntrySize; + } + } + + return null; + } + + /// <summary> + /// Free a <see cref="UpsamplerState"/> at the given index. + /// </summary> + /// <param name="index">The index of the <see cref="UpsamplerState"/> to free.</param> + public void Free(int index) + { + lock (Lock) + { + Debug.Assert(_upsamplers[index] != null); + + _upsamplers[index] = null; + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Upsampler/UpsamplerState.cs b/Ryujinx.Audio/Renderer/Server/Upsampler/UpsamplerState.cs new file mode 100644 index 00000000..8ea55384 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Upsampler/UpsamplerState.cs @@ -0,0 +1,80 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using System; + +namespace Ryujinx.Audio.Renderer.Server.Upsampler +{ + /// <summary> + /// Server state for a upsampling. + /// </summary> + public class UpsamplerState + { + /// <summary> + /// The output buffer containing the target samples. + /// </summary> + public Memory<float> OutputBuffer { get; } + + /// <summary> + /// The target sample count. + /// </summary> + public uint SampleCount { get; } + + /// <summary> + /// The index of the <see cref="UpsamplerState"/>. (used to free it) + /// </summary> + private int _index; + + /// <summary> + /// The <see cref="UpsamplerManager"/>. + /// </summary> + private UpsamplerManager _manager; + + /// <summary> + /// The source sample count. + /// </summary> + public uint SourceSampleCount; + + /// <summary> + /// The input buffer indices of the buffers holding the samples that need upsampling. + /// </summary> + public ushort[] InputBufferIndices; + + /// <summary> + /// Create a new <see cref="UpsamplerState"/>. + /// </summary> + /// <param name="manager">The upsampler manager.</param> + /// <param name="index">The index of the <see cref="UpsamplerState"/>. (used to free it)</param> + /// <param name="outputBuffer">The output buffer used to contain the target samples.</param> + /// <param name="sampleCount">The target sample count.</param> + public UpsamplerState(UpsamplerManager manager, int index, Memory<float> outputBuffer, uint sampleCount) + { + _manager = manager; + _index = index; + OutputBuffer = outputBuffer; + SampleCount = sampleCount; + } + + /// <summary> + /// Release the <see cref="UpsamplerState"/>. + /// </summary> + public void Release() + { + _manager.Free(_index); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Voice/VoiceChannelResource.cs b/Ryujinx.Audio/Renderer/Server/Voice/VoiceChannelResource.cs new file mode 100644 index 00000000..00520cfc --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Voice/VoiceChannelResource.cs @@ -0,0 +1,57 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Voice +{ + /// <summary> + /// Server state for a voice channel resource. + /// </summary> + [StructLayout(LayoutKind.Sequential, Size = 0xD0, Pack = Alignment)] + public struct VoiceChannelResource + { + public const int Alignment = 0x10; + + /// <summary> + /// Mix volumes for the resource. + /// </summary> + public Array24<float> Mix; + + /// <summary> + /// Previous mix volumes for resource. + /// </summary> + public Array24<float> PreviousMix; + + /// <summary> + /// The id of the resource. + /// </summary> + public uint Id; + + /// <summary> + /// Indicate if the resource is used. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool IsUsed; + + public void UpdateState() + { + Mix.ToSpan().CopyTo(PreviousMix.ToSpan()); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Voice/VoiceContext.cs b/Ryujinx.Audio/Renderer/Server/Voice/VoiceContext.cs new file mode 100644 index 00000000..0dfaa4d5 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Voice/VoiceContext.cs @@ -0,0 +1,166 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Utils; +using System; +using System.Diagnostics; + +namespace Ryujinx.Audio.Renderer.Server.Voice +{ + /// <summary> + /// Voice context. + /// </summary> + public class VoiceContext + { + /// <summary> + /// Storage of the sorted indices to <see cref="VoiceState"/>. + /// </summary> + private Memory<int> _sortedVoices; + + /// <summary> + /// Storage for <see cref="VoiceState"/>. + /// </summary> + private Memory<VoiceState> _voices; + + /// <summary> + /// Storage for <see cref="VoiceChannelResource"/>. + /// </summary> + private Memory<VoiceChannelResource> _voiceChannelResources; + + /// <summary> + /// Storage for <see cref="VoiceUpdateState"/> that are used during audio renderer server updates. + /// </summary> + private Memory<VoiceUpdateState> _voiceUpdateStatesCpu; + + /// <summary> + /// Storage for <see cref="VoiceUpdateState"/> for the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + private Memory<VoiceUpdateState> _voiceUpdateStatesDsp; + + /// <summary> + /// The total voice count. + /// </summary> + private uint _voiceCount; + + public void Initialize(Memory<int> sortedVoices, Memory<VoiceState> voices, Memory<VoiceChannelResource> voiceChannelResources, Memory<VoiceUpdateState> voiceUpdateStatesCpu, Memory<VoiceUpdateState> voiceUpdateStatesDsp, uint voiceCount) + { + _sortedVoices = sortedVoices; + _voices = voices; + _voiceChannelResources = voiceChannelResources; + _voiceUpdateStatesCpu = voiceUpdateStatesCpu; + _voiceUpdateStatesDsp = voiceUpdateStatesDsp; + _voiceCount = voiceCount; + } + + /// <summary> + /// Get the total voice count. + /// </summary> + /// <returns>The total voice count.</returns> + public uint GetCount() + { + return _voiceCount; + } + + /// <summary> + /// Get a reference to a <see cref="VoiceChannelResource"/> at the given <paramref name="id"/>. + /// </summary> + /// <param name="id">The index to use.</param> + /// <returns>A reference to a <see cref="VoiceChannelResource"/> at the given <paramref name="id"/>.</returns> + public ref VoiceChannelResource GetChannelResource(int id) + { + return ref SpanIOHelper.GetFromMemory(_voiceChannelResources, id, _voiceCount); + } + + /// <summary> + /// Get a <see cref="Memory{VoiceUpdateState}"/> at the given <paramref name="id"/>. + /// </summary> + /// <param name="id">The index to use.</param> + /// <returns>A <see cref="Memory{VoiceUpdateState}"/> at the given <paramref name="id"/>.</returns> + /// <remarks>The returned <see cref="Memory{VoiceUpdateState}"/> should only be used when updating the server state.</remarks> + public Memory<VoiceUpdateState> GetUpdateStateForCpu(int id) + { + return SpanIOHelper.GetMemory(_voiceUpdateStatesCpu, id, _voiceCount); + } + + /// <summary> + /// Get a <see cref="Memory{VoiceUpdateState}"/> at the given <paramref name="id"/>. + /// </summary> + /// <param name="id">The index to use.</param> + /// <returns>A <see cref="Memory{VoiceUpdateState}"/> at the given <paramref name="id"/>.</returns> + /// <remarks>The returned <see cref="Memory{VoiceUpdateState}"/> should only be used in the context of processing on the <see cref="Dsp.AudioProcessor"/>.</remarks> + public Memory<VoiceUpdateState> GetUpdateStateForDsp(int id) + { + return SpanIOHelper.GetMemory(_voiceUpdateStatesDsp, id, _voiceCount); + } + + /// <summary> + /// Get a reference to a <see cref="VoiceState"/> at the given <paramref name="id"/>. + /// </summary> + /// <param name="id">The index to use.</param> + /// <returns>A reference to a <see cref="VoiceState"/> at the given <paramref name="id"/>.</returns> + public ref VoiceState GetState(int id) + { + return ref SpanIOHelper.GetFromMemory(_voices, id, _voiceCount); + } + + public ref VoiceState GetSortedState(int id) + { + Debug.Assert(id >= 0 && id < _voiceCount); + + return ref GetState(_sortedVoices.Span[id]); + } + + /// <summary> + /// Update internal state during command generation. + /// </summary> + public void UpdateForCommandGeneration() + { + _voiceUpdateStatesDsp.CopyTo(_voiceUpdateStatesCpu); + } + + /// <summary> + /// Sort the internal voices by priority and sorting order (if the priorities match). + /// </summary> + public void Sort() + { + for (int i = 0; i < _voiceCount; i++) + { + _sortedVoices.Span[i] = i; + } + + int[] sortedVoicesTemp = _sortedVoices.Slice(0, (int)GetCount()).ToArray(); + + Array.Sort(sortedVoicesTemp, (a, b) => + { + ref VoiceState aState = ref GetState(a); + ref VoiceState bState = ref GetState(b); + + int result = aState.Priority.CompareTo(bState.Priority); + + if (result == 0) + { + return aState.SortingOrder.CompareTo(bState.SortingOrder); + } + + return result; + }); + + sortedVoicesTemp.AsSpan().CopyTo(_sortedVoices.Span); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Voice/VoiceState.cs b/Ryujinx.Audio/Renderer/Server/Voice/VoiceState.cs new file mode 100644 index 00000000..89abe9aa --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Voice/VoiceState.cs @@ -0,0 +1,716 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Common; +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Server.MemoryPool; +using Ryujinx.Common.Memory; +using Ryujinx.Common.Utilities; +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using static Ryujinx.Audio.Renderer.Common.BehaviourParameter; +using static Ryujinx.Audio.Renderer.Parameter.VoiceInParameter; + +namespace Ryujinx.Audio.Renderer.Server.Voice +{ + [StructLayout(LayoutKind.Sequential, Pack = Alignment)] + public struct VoiceState + { + public const int Alignment = 0x10; + + /// <summary> + /// Set to true if the voice is used. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool InUse; + + /// <summary> + /// Set to true if the voice is new. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool IsNew; + + [MarshalAs(UnmanagedType.I1)] + public bool WasPlaying; + + /// <summary> + /// The <see cref="SampleFormat"/> of the voice. + /// </summary> + public SampleFormat SampleFormat; + + /// <summary> + /// The sample rate of the voice. + /// </summary> + public uint SampleRate; + + /// <summary> + /// The total channel count used. + /// </summary> + public uint ChannelsCount; + + /// <summary> + /// Id of the voice. + /// </summary> + public int Id; + + /// <summary> + /// Node id of the voice. + /// </summary> + public int NodeId; + + /// <summary> + /// The target mix id of the voice. + /// </summary> + public int MixId; + + /// <summary> + /// The current voice <see cref="Types.PlayState"/>. + /// </summary> + public Types.PlayState PlayState; + + /// <summary> + /// The previous voice <see cref="Types.PlayState"/>. + /// </summary> + public Types.PlayState PreviousPlayState; + + /// <summary> + /// The priority of the voice. + /// </summary> + public uint Priority; + + /// <summary> + /// Target sorting position of the voice. (used to sort voice with the same <see cref="Priority"/>) + /// </summary> + public uint SortingOrder; + + /// <summary> + /// The pitch used on the voice. + /// </summary> + public float Pitch; + + /// <summary> + /// The output volume of the voice. + /// </summary> + public float Volume; + + /// <summary> + /// The previous output volume of the voice. + /// </summary> + public float PreviousVolume; + + /// <summary> + /// Biquad filters to apply to the output of the voice. + /// </summary> + public Array2<BiquadFilterParameter> BiquadFilters; + + /// <summary> + /// Total count of <see cref="WaveBufferInternal"/> of the voice. + /// </summary> + public uint WaveBuffersCount; + + /// <summary> + /// Current playing <see cref="WaveBufferInternal"/> of the voice. + /// </summary> + public uint WaveBuffersIndex; + + /// <summary> + /// Change the behaviour of the voice. + /// </summary> + /// <remarks>This was added on REV5.</remarks> + public DecodingBehaviour DecodingBehaviour; + + /// <summary> + /// User state <see cref="AddressInfo"/> required by the data source. + /// </summary> + /// <remarks>Only used for <see cref="SampleFormat.Adpcm"/> as the GC-ADPCM coefficients.</remarks> + public AddressInfo DataSourceStateAddressInfo; + + /// <summary> + /// The wavebuffers of this voice. + /// </summary> + public Array4<WaveBuffer> WaveBuffers; + + /// <summary> + /// The channel resource ids associated to the voice. + /// </summary> + public Array6<int> ChannelResourceIds; + + /// <summary> + /// The target splitter id of the voice. + /// </summary> + public uint SplitterId; + + /// <summary> + /// Change the Sample Rate Conversion (SRC) quality of the voice. + /// </summary> + /// <remarks>This was added on REV8.</remarks> + public SampleRateConversionQuality SrcQuality; + + /// <summary> + /// If set to true, the voice was dropped. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool VoiceDropFlag; + + /// <summary> + /// Set to true if the data source state work buffer wasn't mapped. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool DataSourceStateUnmapped; + + /// <summary> + /// Set to true if any of the <see cref="WaveBuffer.BufferAddressInfo"/> work buffer wasn't mapped. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool BufferInfoUnmapped; + + /// <summary> + /// The biquad filter initialization state storage. + /// </summary> + private BiquadFilterNeedInitializationArrayStruct _biquadFilterNeedInitialization; + + /// <summary> + /// Flush the amount of wavebuffer specified. This will result in the wavebuffer being skipped and marked played. + /// </summary> + /// <remarks>This was added on REV5.</remarks> + public byte FlushWaveBufferCount; + + [StructLayout(LayoutKind.Sequential, Size = Constants.VoiceBiquadFilterCount)] + private struct BiquadFilterNeedInitializationArrayStruct { } + + /// <summary> + /// The biquad filter initialization state array. + /// </summary> + public Span<bool> BiquadFilterNeedInitialization => SpanHelpers.AsSpan<BiquadFilterNeedInitializationArrayStruct, bool>(ref _biquadFilterNeedInitialization); + + /// <summary> + /// Initialize the <see cref="VoiceState"/>. + /// </summary> + public void Initialize() + { + IsNew = false; + VoiceDropFlag = false; + DataSourceStateUnmapped = false; + BufferInfoUnmapped = false; + FlushWaveBufferCount = 0; + PlayState = Types.PlayState.Stopped; + Priority = Constants.VoiceLowestPriority; + Id = 0; + NodeId = 0; + SampleRate = 0; + SampleFormat = SampleFormat.Invalid; + ChannelsCount = 0; + Pitch = 0.0f; + Volume= 0.0f; + PreviousVolume = 0.0f; + BiquadFilters.ToSpan().Fill(new BiquadFilterParameter()); + WaveBuffersCount = 0; + WaveBuffersIndex = 0; + MixId = Constants.UnusedMixId; + SplitterId = Constants.UnusedSplitterId; + DataSourceStateAddressInfo.Setup(0, 0); + + InitializeWaveBuffers(); + } + + /// <summary> + /// Initialize the <see cref="WaveBuffer"/> in this <see cref="VoiceState"/>. + /// </summary> + private void InitializeWaveBuffers() + { + for (int i = 0; i < WaveBuffers.Length; i++) + { + WaveBuffers[i].StartSampleOffset = 0; + WaveBuffers[i].EndSampleOffset = 0; + WaveBuffers[i].ShouldLoop = false; + WaveBuffers[i].IsEndOfStream = false; + WaveBuffers[i].BufferAddressInfo.Setup(0, 0); + WaveBuffers[i].ContextAddressInfo.Setup(0, 0); + WaveBuffers[i].IsSendToAudioProcessor = true; + } + } + + /// <summary> + /// Check if the voice needs to be skipped. + /// </summary> + /// <returns>Returns true if the voice needs to be skipped.</returns> + public bool ShouldSkip() + { + return !InUse || WaveBuffersCount == 0 || DataSourceStateUnmapped || BufferInfoUnmapped || VoiceDropFlag; + } + + /// <summary> + /// Return true if the mix has any destinations. + /// </summary> + /// <returns>True if the mix has any destinations.</returns> + public bool HasAnyDestination() + { + return MixId != Constants.UnusedMixId || SplitterId != Constants.UnusedSplitterId; + } + + /// <summary> + /// Indicate if the server voice information needs to be updated. + /// </summary> + /// <param name="parameter">The user parameter.</param> + /// <returns>Return true, if the server voice information needs to be updated.</returns> + private bool ShouldUpdateParameters(ref VoiceInParameter parameter) + { + if (DataSourceStateAddressInfo.CpuAddress == parameter.DataSourceStateAddress) + { + return DataSourceStateAddressInfo.Size != parameter.DataSourceStateSize; + } + + return DataSourceStateAddressInfo.CpuAddress != parameter.DataSourceStateAddress || + DataSourceStateAddressInfo.Size != parameter.DataSourceStateSize || + DataSourceStateUnmapped; + } + + /// <summary> + /// Update the internal state from a user parameter. + /// </summary> + /// <param name="outErrorInfo">The possible <see cref="ErrorInfo"/> that was generated.</param> + /// <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) + { + InUse = parameter.InUse; + Id = parameter.Id; + NodeId = parameter.NodeId; + + UpdatePlayState(parameter.PlayState); + + SrcQuality = parameter.SrcQuality; + + Priority = parameter.Priority; + SortingOrder = parameter.SortingOrder; + SampleRate = parameter.SampleRate; + SampleFormat = parameter.SampleFormat; + ChannelsCount = parameter.ChannelCount; + Pitch = parameter.Pitch; + Volume = parameter.Volume; + parameter.BiquadFilters.ToSpan().CopyTo(BiquadFilters.ToSpan()); + WaveBuffersCount = parameter.WaveBuffersCount; + WaveBuffersIndex = parameter.WaveBuffersIndex; + + if (behaviourContext.IsFlushVoiceWaveBuffersSupported()) + { + FlushWaveBufferCount += parameter.FlushWaveBufferCount; + } + + MixId = parameter.MixId; + + if (behaviourContext.IsSplitterSupported()) + { + SplitterId = parameter.SplitterId; + } + else + { + SplitterId = Constants.UnusedSplitterId; + } + + parameter.ChannelResourceIds.ToSpan().CopyTo(ChannelResourceIds.ToSpan()); + + DecodingBehaviour behaviour = DecodingBehaviour.Default; + + if (behaviourContext.IsDecodingBehaviourFlagSupported()) + { + behaviour = parameter.DecodingBehaviourFlags; + } + + DecodingBehaviour = behaviour; + + if (parameter.ResetVoiceDropFlag) + { + VoiceDropFlag = false; + } + + if (ShouldUpdateParameters(ref parameter)) + { + DataSourceStateUnmapped = !poolMapper.TryAttachBuffer(out outErrorInfo, ref DataSourceStateAddressInfo, parameter.DataSourceStateAddress, parameter.DataSourceStateSize); + } + else + { + outErrorInfo = new ErrorInfo(); + } + } + + /// <summary> + /// Update the internal play state from user play state. + /// </summary> + /// <param name="userPlayState">The target user play state.</param> + public void UpdatePlayState(PlayState userPlayState) + { + Types.PlayState oldServerPlayState = PlayState; + + PreviousPlayState = oldServerPlayState; + + Types.PlayState newServerPlayState; + + switch (userPlayState) + { + case Common.PlayState.Start: + newServerPlayState = Types.PlayState.Started; + break; + + case Common.PlayState.Stop: + if (oldServerPlayState == Types.PlayState.Stopped) + { + return; + } + + newServerPlayState = Types.PlayState.Stopping; + break; + + case Common.PlayState.Pause: + newServerPlayState = Types.PlayState.Paused; + break; + + default: + throw new NotImplementedException($"Unhandled PlayState.{userPlayState}"); + } + + PlayState = newServerPlayState; + } + + /// <summary> + /// Write the status of the voice to the given user output. + /// </summary> + /// <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, Memory<VoiceUpdateState>[] voiceUpdateStates) + { +#if DEBUG + // Sanity check in debug mode of the internal state + if (!parameter.IsNew && !IsNew) + { + for (int i = 1; i < ChannelsCount; i++) + { + ref VoiceUpdateState stateA = ref voiceUpdateStates[i - 1].Span[0]; + ref VoiceUpdateState stateB = ref voiceUpdateStates[i].Span[0]; + + Debug.Assert(stateA.WaveBufferConsumed == stateB.WaveBufferConsumed); + Debug.Assert(stateA.PlayedSampleCount == stateB.PlayedSampleCount); + Debug.Assert(stateA.Offset == stateB.Offset); + Debug.Assert(stateA.WaveBufferIndex == stateB.WaveBufferIndex); + Debug.Assert(stateA.Fraction == stateB.Fraction); + Debug.Assert(stateA.IsWaveBufferValid.SequenceEqual(stateB.IsWaveBufferValid)); + } + } +#endif + if (parameter.IsNew || IsNew) + { + IsNew = true; + + outStatus.VoiceDropFlag = false; + outStatus.PlayedWaveBuffersCount = 0; + outStatus.PlayedSampleCount = 0; + } + else + { + ref VoiceUpdateState state = ref voiceUpdateStates[0].Span[0]; + + outStatus.VoiceDropFlag = VoiceDropFlag; + outStatus.PlayedWaveBuffersCount = state.WaveBufferConsumed; + outStatus.PlayedSampleCount = state.PlayedSampleCount; + } + } + + /// <summary> + /// Update the internal state of all the <see cref="WaveBuffer"/> of the <see cref="VoiceState"/>. + /// </summary> + /// <param name="errorInfos">An array of <see cref="ErrorInfo"/> used to report errors when mapping any of the <see cref="WaveBuffer"/>.</param> + /// <param name="parameter">The user parameter.</param> + /// <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, Memory<VoiceUpdateState>[] voiceUpdateStates, ref PoolMapper mapper, ref BehaviourContext behaviourContext) + { + errorInfos = new ErrorInfo[Constants.VoiceWaveBufferCount * 2]; + + if (parameter.IsNew) + { + InitializeWaveBuffers(); + + for (int i = 0; i < parameter.ChannelCount; i++) + { + voiceUpdateStates[i].Span[0].IsWaveBufferValid.Fill(false); + } + } + + ref VoiceUpdateState voiceUpdateState = ref voiceUpdateStates[0].Span[0]; + + for (int i = 0; i < Constants.VoiceWaveBufferCount; i++) + { + UpdateWaveBuffer(errorInfos.AsSpan().Slice(i * 2, 2), ref WaveBuffers[i], ref parameter.WaveBuffers[i], parameter.SampleFormat, voiceUpdateState.IsWaveBufferValid[i], ref mapper, ref behaviourContext); + } + } + + /// <summary> + /// Update the internal state of one of the <see cref="WaveBuffer"/> of the <see cref="VoiceState"/>. + /// </summary> + /// <param name="errorInfos">A <see cref="Span{ErrorInfo}"/> used to report errors when mapping the <see cref="WaveBuffer"/>.</param> + /// <param name="waveBuffer">The <see cref="WaveBuffer"/> to update.</param> + /// <param name="inputWaveBuffer">The <see cref="WaveBufferInternal"/> from the user input.</param> + /// <param name="sampleFormat">The <see cref="SampleFormat"/> from the user input.</param> + /// <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) + { + if (!isValid && waveBuffer.IsSendToAudioProcessor && waveBuffer.BufferAddressInfo.CpuAddress != 0) + { + mapper.ForceUnmap(ref waveBuffer.BufferAddressInfo); + waveBuffer.BufferAddressInfo.Setup(0, 0); + } + + if (!inputWaveBuffer.SentToServer || BufferInfoUnmapped) + { + if (inputWaveBuffer.IsSampleOffsetValid(sampleFormat)) + { + Debug.Assert(waveBuffer.IsSendToAudioProcessor); + + waveBuffer.IsSendToAudioProcessor = false; + waveBuffer.StartSampleOffset = inputWaveBuffer.StartSampleOffset; + waveBuffer.EndSampleOffset = inputWaveBuffer.EndSampleOffset; + waveBuffer.ShouldLoop = inputWaveBuffer.ShouldLoop; + waveBuffer.IsEndOfStream = inputWaveBuffer.IsEndOfStream; + waveBuffer.LoopStartSampleOffset = inputWaveBuffer.LoopFirstSampleOffset; + waveBuffer.LoopEndSampleOffset = inputWaveBuffer.LoopLastSampleOffset; + waveBuffer.LoopCount = inputWaveBuffer.LoopCount; + + BufferInfoUnmapped = !mapper.TryAttachBuffer(out ErrorInfo bufferInfoError, ref waveBuffer.BufferAddressInfo, inputWaveBuffer.Address, inputWaveBuffer.Size); + + errorInfos[0] = bufferInfoError; + + if (sampleFormat == SampleFormat.Adpcm && behaviourContext.IsAdpcmLoopContextBugFixed() && inputWaveBuffer.ContextAddress != 0) + { + bool adpcmLoopContextMapped = mapper.TryAttachBuffer(out ErrorInfo adpcmLoopContextInfoError, + ref waveBuffer.ContextAddressInfo, + inputWaveBuffer.ContextAddress, + inputWaveBuffer.ContextSize); + + errorInfos[1] = adpcmLoopContextInfoError; + + if (adpcmLoopContextMapped) + { + BufferInfoUnmapped = DataSourceStateUnmapped; + } + else + { + BufferInfoUnmapped = true; + } + } + else + { + waveBuffer.ContextAddressInfo.Setup(0, 0); + } + } + else + { + errorInfos[0].ErrorCode = ResultCode.InvalidAddressInfo; + errorInfos[0].ExtraErrorInfo = inputWaveBuffer.Address; + } + } + } + + /// <summary> + /// Reset the resources associated to this <see cref="VoiceState"/>. + /// </summary> + /// <param name="context">The voice context.</param> + private void ResetResources(VoiceContext context) + { + for (int i = 0; i < ChannelsCount; i++) + { + int channelResourceId = ChannelResourceIds[i]; + + ref VoiceChannelResource voiceChannelResource = ref context.GetChannelResource(channelResourceId); + + Debug.Assert(voiceChannelResource.IsUsed); + + Memory<VoiceUpdateState> dspSharedState = context.GetUpdateStateForDsp(channelResourceId); + + MemoryMarshal.Cast<VoiceUpdateState, byte>(dspSharedState.Span).Fill(0); + + voiceChannelResource.UpdateState(); + } + } + + /// <summary> + /// Flush a certain amount of <see cref="WaveBuffer"/>. + /// </summary> + /// <param name="waveBufferCount">The amount of wavebuffer to flush.</param> + /// <param name="voiceUpdateStates">The voice states associated to the <see cref="VoiceState"/>.</param> + /// <param name="channelCount">The channel count from user input.</param> + private void FlushWaveBuffers(uint waveBufferCount, Memory<VoiceUpdateState>[] voiceUpdateStates, uint channelCount) + { + uint waveBufferIndex = WaveBuffersIndex; + + for (int i = 0; i < waveBufferCount; i++) + { + WaveBuffers[(int)waveBufferIndex].IsSendToAudioProcessor = true; + + for (int j = 0; j < channelCount; j++) + { + ref VoiceUpdateState voiceUpdateState = ref voiceUpdateStates[j].Span[0]; + + voiceUpdateState.WaveBufferIndex = (voiceUpdateState.WaveBufferIndex + 1) % Constants.VoiceWaveBufferCount; + voiceUpdateState.WaveBufferConsumed++; + voiceUpdateState.IsWaveBufferValid[(int)waveBufferIndex] = false; + } + + waveBufferIndex = (waveBufferIndex + 1) % Constants.VoiceWaveBufferCount; + } + } + + /// <summary> + /// Update the internal parameters for command generation. + /// </summary> + /// <param name="voiceUpdateStates">The voice states associated to the <see cref="VoiceState"/>.</param> + /// <returns>Return true if this voice should be played.</returns> + public bool UpdateParametersForCommandGeneration(Memory<VoiceUpdateState>[] voiceUpdateStates) + { + if (FlushWaveBufferCount != 0) + { + FlushWaveBuffers(FlushWaveBufferCount, voiceUpdateStates, ChannelsCount); + + FlushWaveBufferCount = 0; + } + + switch (PlayState) + { + case Types.PlayState.Started: + for (int i = 0; i < WaveBuffers.Length; i++) + { + ref WaveBuffer wavebuffer = ref WaveBuffers[i]; + + if (!wavebuffer.IsSendToAudioProcessor) + { + for (int y = 0; y < ChannelsCount; y++) + { + Debug.Assert(!voiceUpdateStates[y].Span[0].IsWaveBufferValid[i]); + + voiceUpdateStates[y].Span[0].IsWaveBufferValid[i] = true; + } + + wavebuffer.IsSendToAudioProcessor = true; + } + } + + WasPlaying = false; + + ref VoiceUpdateState primaryVoiceUpdateState = ref voiceUpdateStates[0].Span[0]; + + for (int i = 0; i < primaryVoiceUpdateState.IsWaveBufferValid.Length; i++) + { + if (primaryVoiceUpdateState.IsWaveBufferValid[i]) + { + return true; + } + } + + return false; + + case Types.PlayState.Stopping: + for (int i = 0; i < WaveBuffers.Length; i++) + { + ref WaveBuffer wavebuffer = ref WaveBuffers[i]; + + wavebuffer.IsSendToAudioProcessor = true; + + for (int j = 0; j < ChannelsCount; j++) + { + ref VoiceUpdateState voiceUpdateState = ref voiceUpdateStates[j].Span[0]; + + if (voiceUpdateState.IsWaveBufferValid[i]) + { + voiceUpdateState.WaveBufferIndex = (voiceUpdateState.WaveBufferIndex + 1) % Constants.VoiceWaveBufferCount; + voiceUpdateState.WaveBufferConsumed++; + } + + voiceUpdateState.IsWaveBufferValid[i] = false; + } + } + + for (int i = 0; i < ChannelsCount; i++) + { + ref VoiceUpdateState voiceUpdateState = ref voiceUpdateStates[i].Span[0]; + + voiceUpdateState.Offset = 0; + voiceUpdateState.PlayedSampleCount = 0; + voiceUpdateState.Pitch.ToSpan().Fill(0); + voiceUpdateState.Fraction = 0; + voiceUpdateState.LoopContext = new Dsp.State.AdpcmLoopContext(); + } + + PlayState = Types.PlayState.Stopped; + WasPlaying = PreviousPlayState == Types.PlayState.Started; + + return WasPlaying; + + case Types.PlayState.Stopped: + case Types.PlayState.Paused: + foreach (ref WaveBuffer wavebuffer in WaveBuffers.ToSpan()) + { + wavebuffer.BufferAddressInfo.GetReference(true); + wavebuffer.ContextAddressInfo.GetReference(true); + } + + if (SampleFormat == SampleFormat.Adpcm) + { + if (DataSourceStateAddressInfo.CpuAddress != 0) + { + DataSourceStateAddressInfo.GetReference(true); + } + } + + WasPlaying = PreviousPlayState == Types.PlayState.Started; + + return WasPlaying; + default: + throw new NotImplementedException($"{PlayState}"); + } + } + + /// <summary> + /// Update the internal state for command generation. + /// </summary> + /// <param name="context">The voice context.</param> + /// <returns>Return true if this voice should be played.</returns> + public bool UpdateForCommandGeneration(VoiceContext context) + { + if (IsNew) + { + ResetResources(context); + PreviousVolume = Volume; + IsNew = false; + } + + Memory<VoiceUpdateState>[] voiceUpdateStates = new Memory<VoiceUpdateState>[Constants.VoiceChannelCountMax]; + + for (int i = 0; i < ChannelsCount; i++) + { + voiceUpdateStates[i] = context.GetUpdateStateForDsp(ChannelResourceIds[i]); + } + + return UpdateParametersForCommandGeneration(voiceUpdateStates); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Voice/WaveBuffer.cs b/Ryujinx.Audio/Renderer/Server/Voice/WaveBuffer.cs new file mode 100644 index 00000000..a03aa757 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Voice/WaveBuffer.cs @@ -0,0 +1,121 @@ +// +// Copyright (c) 2019-2021 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Server.MemoryPool; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Voice +{ + /// <summary> + /// A wavebuffer used for server update. + /// </summary> + [StructLayout(LayoutKind.Sequential, Size = 0x58, Pack = 1)] + public struct WaveBuffer + { + /// <summary> + /// The <see cref="AddressInfo"/> of the sample data of the wavebuffer. + /// </summary> + public AddressInfo BufferAddressInfo; + + /// <summary> + /// The <see cref="AddressInfo"/> of the context of the wavebuffer. + /// </summary> + /// <remarks>Only used by <see cref="Common.SampleFormat.Adpcm"/>.</remarks> + public AddressInfo ContextAddressInfo; + + + /// <summary> + /// First sample to play of the wavebuffer. + /// </summary> + public uint StartSampleOffset; + + /// <summary> + /// Last sample to play of the wavebuffer. + /// </summary> + public uint EndSampleOffset; + + /// <summary> + /// Set to true if the wavebuffer is looping. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool ShouldLoop; + + /// <summary> + /// Set to true if the wavebuffer is the end of stream. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool IsEndOfStream; + + /// <summary> + /// Set to true if the wavebuffer wasn't sent to the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool IsSendToAudioProcessor; + + /// <summary> + /// First sample to play when looping the wavebuffer. + /// </summary> + public uint LoopStartSampleOffset; + + /// <summary> + /// Last sample to play when looping the wavebuffer. + /// </summary> + public uint LoopEndSampleOffset; + + /// <summary> + /// The max loop count. + /// </summary> + public int LoopCount; + + /// <summary> + /// Create a new <see cref="Common.WaveBuffer"/> for use by the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + /// <param name="version">The target version of the wavebuffer.</param> + /// <returns>A new <see cref="Common.WaveBuffer"/> for use by the <see cref="Dsp.AudioProcessor"/>.</returns> + public Common.WaveBuffer ToCommon(int version) + { + Common.WaveBuffer waveBuffer = new Common.WaveBuffer(); + + waveBuffer.Buffer = BufferAddressInfo.GetReference(true); + waveBuffer.BufferSize = (uint)BufferAddressInfo.Size; + + if (ContextAddressInfo.CpuAddress != 0) + { + waveBuffer.Context = ContextAddressInfo.GetReference(true); + waveBuffer.ContextSize = (uint)ContextAddressInfo.Size; + } + + waveBuffer.StartSampleOffset = StartSampleOffset; + waveBuffer.EndSampleOffset = EndSampleOffset; + waveBuffer.Looping = ShouldLoop; + waveBuffer.IsEndOfStream = IsEndOfStream; + + if (version == 2) + { + waveBuffer.LoopCount = LoopCount; + waveBuffer.LoopStartSampleOffset = LoopStartSampleOffset; + waveBuffer.LoopEndSampleOffset = LoopEndSampleOffset; + } + else + { + waveBuffer.LoopCount = -1; + } + + return waveBuffer; + } + } +} |
