Emulator/Gba.cs
namespace sGBA;
public class Gba
{
public GbaTiming Timing { get; } = new();
public ArmCore Cpu { get; private set; }
public GbaMemory Memory { get; private set; }
public GbaVideo Video { get; private set; }
public GbaIo Io { get; private set; }
public GbaDmaController Dma { get; private set; }
public GbaTimerController Timers { get; private set; }
public GbaBios Bios { get; private set; }
public GbaAudio Audio { get; private set; }
public GbaSavedata Savedata { get; private set; }
public GbaCartridgeHardware Hardware { get; private set; }
public bool IsRunning { get; set; }
public int CyclesThisFrame { get; set; }
public long FrameCounter { get; set; }
public long TotalCycles { get; set; }
public ushort KeysActive { get; set; }
public ushort KeysLast { get; set; } = 0x400;
public bool AllowOpposingDirections { get; set; } = true;
public bool HaltPending { get; set; }
public Gba()
{
Memory = new GbaMemory( this );
Cpu = new ArmCore( this );
Video = new GbaVideo( this );
Io = new GbaIo( this );
Dma = new GbaDmaController( this );
Timers = new GbaTimerController( this );
Bios = new GbaBios( this );
Audio = new GbaAudio( this );
Savedata = new GbaSavedata( this );
Hardware = new GbaCartridgeHardware( this );
}
public void LoadRom( byte[] romData )
{
Memory.LoadRom( romData );
Savedata.ForceType( romData );
Hardware.InitRtc( romData );
}
public void Reset()
{
Timing.Clear();
Memory.Reset();
Cpu.Reset();
Video.Reset();
Io.Reset();
Dma.Reset();
Timers.Reset();
Audio.Reset();
Savedata.Reset();
Hardware.Reset();
Memory.InstallHleBios();
Cpu.SkipBios();
Io.ApplySkipBiosState();
KeysLast = 0x400;
HaltPending = false;
CyclesThisFrame = 0;
FrameCounter = 0;
TotalCycles = 0;
IsRunning = true;
_frameInProgress = false;
}
public void RunFrame()
{
if ( !IsRunning ) return;
Audio.BeginFrame();
Io.TestKeypadIrq();
long startCounter = FrameCounter;
while ( IsRunning && FrameCounter == startCounter )
{
long next = Timing.NextEvent;
if ( next == long.MaxValue )
break;
if ( next <= Cpu.Cycles )
{
DrainDueEvents();
continue;
}
RunCpuTo( next );
if ( Cpu.Cycles < next )
break;
}
}
private void DrainDueEvents()
{
Io.BeginEventProcessing();
Timing.Tick( Cpu.Cycles );
Io.EndEventProcessing();
}
private bool _frameInProgress;
private long _frameStartCounter;
private const int StepFrameEventBudget = 4096;
public bool FrameInProgress => _frameInProgress;
public bool StepFrame()
{
if ( !IsRunning ) return false;
if ( Io.LockstepBlocked ) return false;
if ( !_frameInProgress )
{
Audio.BeginFrame();
Io.TestKeypadIrq();
_frameStartCounter = FrameCounter;
_frameInProgress = true;
}
int budget = 0;
while ( FrameCounter == _frameStartCounter )
{
long next = Timing.NextEvent;
if ( next == long.MaxValue )
break;
if ( next <= Cpu.Cycles )
{
DrainDueEvents();
continue;
}
RunCpuTo( next );
if ( Io.LockstepBlocked || Cpu.Cycles < next )
return false;
if ( ++budget >= StepFrameEventBudget )
return false;
}
_frameInProgress = false;
return true;
}
private void RunCpuTo( long target )
{
while ( Cpu.Cycles < target )
{
if ( Io.LockstepBlocked )
{
if ( Io.NextDriverEvent <= Cpu.Cycles )
Io.ProcessDriverEvent();
if ( Io.LockstepBlocked )
return;
}
if ( Timing.NextEvent <= Cpu.Cycles )
{
DrainDueEvents();
continue;
}
if ( Cpu.Halted )
{
long next = Math.Min( Timing.NextEvent, target );
if ( next > Cpu.Cycles )
Cpu.Cycles = next;
continue;
}
Cpu.Run( target );
}
}
public void SetKeyState( GbaKey key, bool pressed )
{
if ( pressed )
KeysActive |= (ushort)key;
else
KeysActive &= (ushort)~(ushort)key;
Io.TestKeypadIrq();
}
}
[Flags]
public enum GbaKey : ushort
{
A = 1 << 0,
B = 1 << 1,
Select = 1 << 2,
Start = 1 << 3,
Right = 1 << 4,
Left = 1 << 5,
Up = 1 << 6,
Down = 1 << 7,
R = 1 << 8,
L = 1 << 9,
}