EmulatorComponent.CoreSync.cs
using System.Threading;
using System.Threading.Tasks;

namespace sGBA;

public sealed partial class EmulatorComponent
{
	private sealed class GbaCoreSync
	{
		public int VideoFramePending { get; private set; }
		public bool VideoFrameWait { get; private set; }
		public bool AudioWait { get; private set; }
		public int AudioHighWater { get; set; }
		public float FpsTarget { get; private set; }

		private readonly object _videoFrameLock = new();
		private readonly object _audioBufferLock = new();
		private readonly SemaphoreSlim _videoFrameAvailable = new( 0, 1 );
		private readonly SemaphoreSlim _videoFrameRequired = new( 0, 1 );
		private readonly SemaphoreSlim _audioRequired = new( 0, 1 );
		private int _audioSamplesPending;

		public void LoadCoreOptions( bool audioSync, bool videoSync, float fpsTarget )
		{
			AudioWait = audioSync;
			VideoFrameWait = videoSync;
			FpsTarget = fpsTarget > 0 ? fpsTarget : 60f;
			AudioHighWater = 512;
		}

		public async Task PostFrameAsync( CancellationToken token )
		{
			lock ( _videoFrameLock )
			{
				VideoFramePending++;
				Signal( _videoFrameAvailable );
			}

			while ( true )
			{
				lock ( _videoFrameLock )
				{
					if ( !VideoFrameWait || VideoFramePending <= 0 )
						return;
				}

				await _videoFrameRequired.WaitAsync( SyncWaitMilliseconds, token );
			}
		}

		public void ForceFrame()
		{
			Signal( _videoFrameAvailable );
			Signal( _videoFrameRequired );
		}

		public void WaitPrologue( out bool videoFrameWait, out bool audioWait )
		{
			lock ( _videoFrameLock )
			{
				videoFrameWait = VideoFrameWait;
				VideoFrameWait = false;
				Signal( _videoFrameAvailable );
				Signal( _videoFrameRequired );
			}

			lock ( _audioBufferLock )
			{
				audioWait = AudioWait;
				AudioWait = false;
				Signal( _audioRequired );
			}
		}

		public void WaitEpilogue( bool videoFrameWait, bool audioWait )
		{
			lock ( _audioBufferLock )
			{
				AudioWait = audioWait;
			}

			lock ( _videoFrameLock )
			{
				VideoFrameWait = videoFrameWait;
				Signal( _videoFrameAvailable );
			}
		}

		public bool WaitFrameStart()
		{
			Monitor.Enter( _videoFrameLock );
			try
			{
				if ( !VideoFrameWait && VideoFramePending <= 0 )
					return false;

				Signal( _videoFrameRequired );

				if ( VideoFrameWait && VideoFramePending <= 0 )
				{
					Drain( _videoFrameAvailable );
					Monitor.Exit( _videoFrameLock );
					try
					{
						if ( !_videoFrameAvailable.Wait( SyncWaitMilliseconds ) )
							return false;
					}
					finally
					{
						Monitor.Enter( _videoFrameLock );
					}
				}

				if ( VideoFramePending <= 0 )
					return false;

				Drain( _videoFrameAvailable );
				VideoFramePending = 0;
				return true;
			}
			finally
			{
				Monitor.Exit( _videoFrameLock );
			}
		}

		public void WaitFrameEnd()
		{
			Signal( _videoFrameRequired );
		}

		public void SetVideoSync( bool wait )
		{
			lock ( _videoFrameLock )
			{
				if ( wait == VideoFrameWait )
					return;

				VideoFrameWait = wait;
				Signal( _videoFrameAvailable );
			}
		}

		public void SetAudioSync( bool wait )
		{
			lock ( _audioBufferLock )
			{
				AudioWait = wait;
				Signal( _audioRequired );
			}
		}

		public async Task ProduceAudioAsync( int audioSamples, CancellationToken token )
		{
			if ( audioSamples <= 0 )
				return;

			lock ( _audioBufferLock )
			{
				_audioSamplesPending += audioSamples;
			}

			while ( true )
			{
				lock ( _audioBufferLock )
				{
					if ( !AudioWait || AudioHighWater <= 0 || _audioSamplesPending < AudioHighWater )
						return;
				}

				await _audioRequired.WaitAsync( SyncWaitMilliseconds, token );
			}
		}

		public void ConsumeAudio( int audioSamples )
		{
			if ( audioSamples > 0 )
			{
				lock ( _audioBufferLock )
				{
					_audioSamplesPending = Math.Max( 0, _audioSamplesPending - audioSamples );
				}
			}

			Signal( _audioRequired );
		}

		private static void Signal( SemaphoreSlim signal )
		{
			try { signal.Release(); }
			catch ( SemaphoreFullException ) { }
		}

		private static void Drain( SemaphoreSlim signal )
		{
			while ( signal.Wait( 0 ) ) { }
		}
	}
}