EmulatorComponent.CoreThread.cs
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
namespace sGBA;
public sealed partial class EmulatorComponent
{
private readonly struct FramePacket( short[] audio, int audioSamples, byte[] saveData )
{
public readonly short[] Audio = audio;
public readonly int AudioSamples = audioSamples;
public readonly byte[] SaveData = saveData;
}
private enum GbaCoreThreadState
{
Initialized = -1,
Running = 0,
Request,
Interrupted,
Paused,
Crashed,
Interrupting,
Exiting,
Shutdown
}
[Flags]
private enum GbaCoreThreadRequest
{
None = 0,
Pause = 1,
Wait = 2,
Reset = 4,
RunOn = 8,
Crashed = 16,
RewindEmpty = 32
}
private sealed class GbaCoreThread
{
public Gba Core { get; }
public GbaCoreSync Sync { get; } = new();
public ConcurrentQueue<FramePacket> PostedFrames { get; } = new();
private readonly ConcurrentQueue<Action<Gba>> _runQueue = new();
private readonly SemaphoreSlim _stateOnThreadSignal = new( 0, 1 );
private readonly object _stateLock = new();
private readonly object _coreLock;
private readonly Func<ushort> _readInputKeysActive;
private readonly Action<string> _logError;
private readonly Action _resetCallback;
private readonly short[][] _audioBuffers;
private CancellationTokenSource _cts;
private Task _workerTask;
private GbaCoreThreadState _state = GbaCoreThreadState.Initialized;
private GbaCoreThreadRequest _requested;
private int _audioBufferIndex;
private bool _waitPrologueActive;
private bool _waitPrologueVideoFrameWait;
private bool _waitPrologueAudioWait;
public GbaCoreThread( Gba core, object coreLock, Func<ushort> readInputKeysActive, Action<string> logError, Action resetCallback )
{
Core = core;
_coreLock = coreLock;
_readInputKeysActive = readInputKeysActive;
_logError = logError;
_resetCallback = resetCallback;
_audioBuffers = new short[4][];
int audioBufferSize = GbaAudio.SamplesPerFrame * 2;
for ( int i = 0; i < _audioBuffers.Length; i++ )
_audioBuffers[i] = new short[audioBufferSize];
}
public void Start()
{
if ( _cts != null )
return;
_cts = new CancellationTokenSource();
ChangeState( GbaCoreThreadState.Running );
_workerTask = GameTask.RunInThreadAsync( Run );
_ = ObserveWorkerTaskAsync( _workerTask );
}
public void End()
{
ChangeState( GbaCoreThreadState.Exiting );
_cts?.Cancel();
Sync.SetAudioSync( false );
Sync.SetVideoSync( false );
Sync.ForceFrame();
SignalStateOnThread();
_cts = null;
}
public void Reset()
{
SendRequest( GbaCoreThreadRequest.Reset );
SignalStateOnThread();
Sync.ForceFrame();
}
public void Pause()
{
SendRequest( GbaCoreThreadRequest.Pause );
WaitPrologue();
SignalStateOnThread();
Sync.ForceFrame();
}
public void Unpause()
{
CancelRequest( GbaCoreThreadRequest.Pause );
WaitEpilogue();
SignalStateOnThread();
Sync.ForceFrame();
}
public void SetPaused( bool paused )
{
if ( paused )
Pause();
else
Unpause();
}
public void RunFunction( Action<Gba> run )
{
if ( run == null || _cts == null )
return;
_runQueue.Enqueue( run );
SendRequest( GbaCoreThreadRequest.RunOn );
SignalStateOnThread();
Sync.ForceFrame();
}
private async Task ObserveWorkerTaskAsync( Task workerTask )
{
try
{
await workerTask;
}
catch ( OperationCanceledException ) { }
catch ( ObjectDisposedException ) { }
catch ( Exception ex )
{
try
{
_logError?.Invoke( $"Emulation worker task error: {ex.Message}\n{ex.StackTrace}" );
}
catch { }
}
finally
{
if ( _workerTask == workerTask )
_workerTask = null;
}
}
private async Task Run()
{
CancellationTokenSource cts = _cts;
if ( cts == null )
return;
CancellationToken token = cts.Token;
try
{
while ( !token.IsCancellationRequested )
{
GbaCoreThreadRequest pendingRequests = TakePendingRequests();
RunPendingRequests( pendingRequests );
if ( IsRequested( GbaCoreThreadRequest.Pause | GbaCoreThreadRequest.Wait | GbaCoreThreadRequest.Crashed | GbaCoreThreadRequest.RewindEmpty ) )
{
ChangeState( GbaCoreThreadState.Paused );
await _stateOnThreadSignal.WaitAsync( SyncWaitMilliseconds, token );
continue;
}
ChangeState( GbaCoreThreadState.Running );
FramePacket frame = RunFrame( token );
PostedFrames.Enqueue( frame );
await Sync.ProduceAudioAsync( frame.AudioSamples, token );
await Sync.PostFrameAsync( token );
await GameTask.Yield();
}
}
catch ( OperationCanceledException ) { }
catch ( ObjectDisposedException ) { }
catch ( Exception ex )
{
SendRequest( GbaCoreThreadRequest.Crashed );
ChangeState( GbaCoreThreadState.Crashed );
try
{
_logError?.Invoke( $"Emulation worker error: {ex.Message}\n{ex.StackTrace}" );
}
catch { }
}
finally
{
ChangeState( GbaCoreThreadState.Shutdown );
}
}
private FramePacket RunFrame( CancellationToken token )
{
short[] audio;
int audioSamples;
byte[] saveData = null;
lock ( _coreLock )
{
Core.KeysActive = _readInputKeysActive();
Core.RunFrame();
if ( token.IsCancellationRequested )
return new FramePacket( null, 0, null );
int bufferIndex = _audioBufferIndex;
_audioBufferIndex = (bufferIndex + 1) & 3;
audio = _audioBuffers[bufferIndex];
audioSamples = Core.Audio.SamplesWritten;
if ( audioSamples > 0 )
Buffer.BlockCopy( Core.Audio.OutputBuffer, 0, audio, 0, audioSamples * 2 * sizeof( short ) );
if ( Core.Savedata.Clean() && Core.Savedata.Data.Length > 0 )
saveData = Core.Savedata.Data.ToArray();
}
return new FramePacket( audio, audioSamples, saveData );
}
private GbaCoreThreadRequest TakePendingRequests()
{
lock ( _stateLock )
{
GbaCoreThreadRequest pendingRequests = _requested;
_requested &= GbaCoreThreadRequest.Pause | GbaCoreThreadRequest.Wait | GbaCoreThreadRequest.Crashed | GbaCoreThreadRequest.RewindEmpty;
return pendingRequests;
}
}
private void RunPendingRequests( GbaCoreThreadRequest pendingRequests )
{
if ( (pendingRequests & GbaCoreThreadRequest.Reset) != 0 )
{
lock ( _coreLock )
{
Core.Reset();
}
_resetCallback?.Invoke();
}
if ( (pendingRequests & GbaCoreThreadRequest.RunOn) != 0 )
RunPendingFunctions();
}
private void RunPendingFunctions()
{
while ( _runQueue.TryDequeue( out Action<Gba> run ) )
{
try
{
lock ( _coreLock )
{
run( Core );
}
}
catch ( Exception ex )
{
GbaLog.Write( LogCategory.GBA, LogLevel.Error, ex.Message );
}
}
}
private void WaitPrologue()
{
if ( _waitPrologueActive )
return;
Sync.WaitPrologue( out _waitPrologueVideoFrameWait, out _waitPrologueAudioWait );
_waitPrologueActive = true;
}
private void WaitEpilogue()
{
if ( !_waitPrologueActive )
return;
Sync.WaitEpilogue( _waitPrologueVideoFrameWait, _waitPrologueAudioWait );
_waitPrologueActive = false;
}
private bool IsRequested( GbaCoreThreadRequest request )
{
lock ( _stateLock )
{
return (_requested & request) != 0;
}
}
private void SendRequest( GbaCoreThreadRequest request )
{
lock ( _stateLock )
{
_requested |= request;
if ( _state is GbaCoreThreadState.Running or GbaCoreThreadState.Paused or GbaCoreThreadState.Crashed )
_state = GbaCoreThreadState.Request;
}
}
private void CancelRequest( GbaCoreThreadRequest request )
{
lock ( _stateLock )
{
_requested &= ~request;
if ( _state == GbaCoreThreadState.Request && _requested == GbaCoreThreadRequest.None )
_state = GbaCoreThreadState.Running;
}
}
private void ChangeState( GbaCoreThreadState state )
{
lock ( _stateLock )
{
_state = state;
}
}
private void SignalStateOnThread()
{
try { _stateOnThreadSignal.Release(); }
catch ( SemaphoreFullException ) { }
}
}
}