Cores/Nds/DMA.cs
namespace Emulotl.Nds;

public sealed class DMA
{
	public uint SrcAddr;
	public uint DstAddr;
	public uint Cnt;

	private readonly NDS NDS;
	private readonly uint CPU;
	private readonly uint Num;

	private uint StartMode;
	private uint CurSrcAddr;
	private uint CurDstAddr;
	private uint RemCount;
	private uint IterCount;
	private int SrcAddrInc;
	private int DstAddrInc;
	private uint CountMask;

	private uint Running;
	private bool InProgress;
	private bool Executing;
	private bool Stall;
	private bool IsGXFIFODMA;

	private byte[] MRAMBurstTable;
	private uint MRAMBurstCount;

	public DMA( uint cpu, uint num, NDS nds )
	{
		CPU = cpu;
		Num = num;
		NDS = nds;

		if ( cpu == 0 )
			CountMask = 0x001FFFFF;
		else
			CountMask = num == 3 ? 0x0000FFFFu : 0x00003FFFu;
	}

	public void Reset()
	{
		SrcAddr = 0;
		DstAddr = 0;
		Cnt = 0;

		StartMode = 0;
		CurSrcAddr = 0;
		CurDstAddr = 0;
		RemCount = 0;
		IterCount = 0;
		SrcAddrInc = 0;
		DstAddrInc = 0;

		Stall = false;

		Running = 0;
		Executing = false;
		InProgress = false;

		MRAMBurstTable = null;
		MRAMBurstCount = 0;
	}

	public bool IsInMode( uint mode ) => mode == StartMode && (Cnt & 0x80000000) != 0;

	public bool IsRunning() => Running != 0;

	public void StartIfNeeded( uint mode )
	{
		if ( mode == StartMode && (Cnt & 0x80000000) != 0 )
			Start();
	}

	public void StopIfNeeded( uint mode )
	{
		if ( mode == StartMode )
			Cnt &= ~0x80000000;
	}

	public void StallIfRunning()
	{
		if ( Executing ) Stall = true;
	}

	public void WriteCnt( uint val )
	{
		uint oldcnt = Cnt;
		Cnt = val;

		if ( (oldcnt & 0x80000000) == 0 && (val & 0x80000000) != 0 )
		{
			CurSrcAddr = SrcAddr;
			CurDstAddr = DstAddr;

			switch ( Cnt & 0x00600000 )
			{
				case 0x00000000: DstAddrInc = 1; break;
				case 0x00200000: DstAddrInc = -1; break;
				case 0x00400000: DstAddrInc = 0; break;
				case 0x00600000: DstAddrInc = 1; break;
			}

			switch ( Cnt & 0x01800000 )
			{
				case 0x00000000: SrcAddrInc = 1; break;
				case 0x00800000: SrcAddrInc = -1; break;
				case 0x01000000: SrcAddrInc = 0; break;
				case 0x01800000: SrcAddrInc = 1; break;
			}

			if ( CPU == 0 )
				StartMode = (Cnt >> 27) & 0x7;
			else
				StartMode = ((Cnt >> 28) & 0x3) | 0x10;

			if ( (StartMode & 0x7) == 0 )
				Start();
			else if ( StartMode == 0x05 || StartMode == 0x12 )
				NDS.CartCheckDMA();
			else if ( StartMode == 0x07 )
				NDS.GPU3D.CheckFIFODMA();

			if ( StartMode == 0x06 || StartMode == 0x13 )
				Platform.Log( LogLevel.Warn, $"UNIMPLEMENTED ARM{(CPU != 0 ? 7 : 9)} DMA{Num} START MODE {StartMode:X2}, {SrcAddr:X8}->{DstAddr:X8}" );
		}
	}

	public void Start()
	{
		if ( Running != 0 ) return;

		if ( !InProgress )
		{
			uint countmask;
			if ( CPU == 0 )
				countmask = 0x001FFFFF;
			else
				countmask = Num == 3 ? 0x0000FFFFu : 0x00003FFFu;

			RemCount = Cnt & countmask;
			if ( RemCount == 0 )
				RemCount = countmask + 1;
		}

		if ( StartMode == 0x07 && RemCount > 112 )
			IterCount = 112;
		else
			IterCount = RemCount;

		if ( (Cnt & 0x01800000) == 0x01800000 )
			CurSrcAddr = SrcAddr;

		if ( (Cnt & 0x00600000) == 0x00600000 )
			CurDstAddr = DstAddr;

		IsGXFIFODMA = CPU == 0 && (CurSrcAddr >> 24) == 0x02 && CurDstAddr == 0x04000400 && DstAddrInc == 0;

		Running = 2;

		InProgress = true;
		NDS.StopCPU( CPU, 1u << (int)Num );
	}

	public void Run()
	{
		if ( Running == 0 ) return;
		if ( CPU == 0 ) Run9();
		else Run7();
	}

	private uint UnitTimings9_16( bool burststart )
	{
		uint src_id = CurSrcAddr >> 14;
		uint dst_id = CurDstAddr >> 14;

		uint src_rgn = NDS.ARM9Regions[src_id];
		uint dst_rgn = NDS.ARM9Regions[dst_id];

		uint src_n = NDS.ARM9MemTimings[src_id, 4];
		uint src_s = NDS.ARM9MemTimings[src_id, 5];
		uint dst_n = NDS.ARM9MemTimings[dst_id, 4];
		uint dst_s = NDS.ARM9MemTimings[dst_id, 5];

		if ( src_rgn == Mem9.MainRAM )
		{
			if ( dst_rgn == Mem9.MainRAM )
				return 16;

			if ( SrcAddrInc > 0 )
			{
				if ( burststart || MRAMBurstTable[MRAMBurstCount] == 0 )
				{
					MRAMBurstCount = 0;

					if ( dst_rgn == Mem9.GBAROM )
						MRAMBurstTable = dst_s == 4 ? DMATiming.MRAMRead16Bursts[1] : DMATiming.MRAMRead16Bursts[2];
					else
						MRAMBurstTable = DMATiming.MRAMRead16Bursts[0];
				}

				return MRAMBurstTable[MRAMBurstCount++];
			}

			uint rbase = ((CurSrcAddr & 0x1F) == 0x1E) ? 7u : 8u;
			return rbase + (burststart ? dst_n : dst_s);
		}

		if ( dst_rgn == Mem9.MainRAM )
		{
			if ( DstAddrInc > 0 )
			{
				if ( burststart || MRAMBurstTable[MRAMBurstCount] == 0 )
				{
					MRAMBurstCount = 0;

					if ( src_rgn == Mem9.GBAROM )
						MRAMBurstTable = src_s == 4 ? DMATiming.MRAMWrite16Bursts[1] : DMATiming.MRAMWrite16Bursts[2];
					else
						MRAMBurstTable = DMATiming.MRAMWrite16Bursts[0];
				}

				return MRAMBurstTable[MRAMBurstCount++];
			}

			return (burststart ? src_n : src_s) + 7;
		}

		if ( (src_rgn & dst_rgn) != 0 )
			return src_n + dst_n + 1;

		return burststart ? src_n + dst_n : src_s + dst_s;
	}

	private uint UnitTimings9_32( bool burststart )
	{
		uint src_id = CurSrcAddr >> 14;
		uint dst_id = CurDstAddr >> 14;

		uint src_rgn = NDS.ARM9Regions[src_id];
		uint dst_rgn = NDS.ARM9Regions[dst_id];

		uint src_n = NDS.ARM9MemTimings[src_id, 6];
		uint src_s = NDS.ARM9MemTimings[src_id, 7];
		uint dst_n = NDS.ARM9MemTimings[dst_id, 6];
		uint dst_s = NDS.ARM9MemTimings[dst_id, 7];

		if ( src_rgn == Mem9.MainRAM )
		{
			if ( dst_rgn == Mem9.MainRAM )
				return 18;

			if ( SrcAddrInc > 0 )
			{
				if ( burststart || MRAMBurstTable[MRAMBurstCount] == 0 )
				{
					MRAMBurstCount = 0;

					if ( dst_rgn == Mem9.GBAROM )
						MRAMBurstTable = dst_s == 8 ? DMATiming.MRAMRead32Bursts[2] : DMATiming.MRAMRead32Bursts[3];
					else if ( dst_n == 2 )
						MRAMBurstTable = DMATiming.MRAMRead32Bursts[0];
					else
						MRAMBurstTable = DMATiming.MRAMRead32Bursts[1];
				}

				return MRAMBurstTable[MRAMBurstCount++];
			}

			uint rbase = ((CurSrcAddr & 0x1F) == 0x1C) ? (dst_n == 2 ? 7u : 8u) : 9u;
			return rbase + (burststart ? dst_n : dst_s);
		}

		if ( dst_rgn == Mem9.MainRAM )
		{
			if ( DstAddrInc > 0 )
			{
				if ( burststart || MRAMBurstTable[MRAMBurstCount] == 0 )
				{
					MRAMBurstCount = 0;

					if ( src_rgn == Mem9.GBAROM )
						MRAMBurstTable = src_s == 8 ? DMATiming.MRAMWrite32Bursts[2] : DMATiming.MRAMWrite32Bursts[3];
					else if ( src_n == 2 )
						MRAMBurstTable = DMATiming.MRAMWrite32Bursts[0];
					else
						MRAMBurstTable = DMATiming.MRAMWrite32Bursts[1];
				}

				return MRAMBurstTable[MRAMBurstCount++];
			}

			return (burststart ? src_n : src_s) + 8;
		}

		if ( (src_rgn & dst_rgn) != 0 )
			return src_n + dst_n + 1;

		return burststart ? src_n + dst_n : src_s + dst_s;
	}

	private uint UnitTimings7_16( bool burststart )
	{
		uint src_id = CurSrcAddr >> 15;
		uint dst_id = CurDstAddr >> 15;

		uint src_rgn = NDS.ARM7Regions[src_id];
		uint dst_rgn = NDS.ARM7Regions[dst_id];

		uint src_n = NDS.ARM7MemTimings[src_id, 0];
		uint src_s = NDS.ARM7MemTimings[src_id, 1];
		uint dst_n = NDS.ARM7MemTimings[dst_id, 0];
		uint dst_s = NDS.ARM7MemTimings[dst_id, 1];

		if ( src_rgn == Mem7.MainRAM )
		{
			if ( dst_rgn == Mem7.MainRAM )
				return 16;

			if ( SrcAddrInc > 0 )
			{
				if ( burststart || MRAMBurstTable[MRAMBurstCount] == 0 )
				{
					MRAMBurstCount = 0;

					if ( dst_rgn == Mem7.GBAROM || dst_rgn == Mem7.Wifi0 || dst_rgn == Mem7.Wifi1 )
						MRAMBurstTable = dst_s == 4 ? DMATiming.MRAMRead16Bursts[1] : DMATiming.MRAMRead16Bursts[2];
					else
						MRAMBurstTable = DMATiming.MRAMRead16Bursts[0];
				}

				return MRAMBurstTable[MRAMBurstCount++];
			}

			uint rbase = ((CurSrcAddr & 0x1F) == 0x1E) ? 7u : 8u;
			return rbase + (burststart ? dst_n : dst_s);
		}

		if ( dst_rgn == Mem7.MainRAM )
		{
			if ( DstAddrInc > 0 )
			{
				if ( burststart || MRAMBurstTable[MRAMBurstCount] == 0 )
				{
					MRAMBurstCount = 0;

					if ( src_rgn == Mem7.GBAROM || src_rgn == Mem7.Wifi0 || src_rgn == Mem7.Wifi1 )
						MRAMBurstTable = src_s == 4 ? DMATiming.MRAMWrite16Bursts[1] : DMATiming.MRAMWrite16Bursts[2];
					else
						MRAMBurstTable = DMATiming.MRAMWrite16Bursts[0];
				}

				return MRAMBurstTable[MRAMBurstCount++];
			}

			return (burststart ? src_n : src_s) + 7;
		}

		if ( (src_rgn & dst_rgn) != 0 )
			return src_n + dst_n + 1;

		return burststart ? src_n + dst_n : src_s + dst_s;
	}

	private uint UnitTimings7_32( bool burststart )
	{
		uint src_id = CurSrcAddr >> 15;
		uint dst_id = CurDstAddr >> 15;

		uint src_rgn = NDS.ARM7Regions[src_id];
		uint dst_rgn = NDS.ARM7Regions[dst_id];

		uint src_n = NDS.ARM7MemTimings[src_id, 2];
		uint src_s = NDS.ARM7MemTimings[src_id, 3];
		uint dst_n = NDS.ARM7MemTimings[dst_id, 2];
		uint dst_s = NDS.ARM7MemTimings[dst_id, 3];

		if ( src_rgn == Mem7.MainRAM )
		{
			if ( dst_rgn == Mem7.MainRAM )
				return 18;

			if ( SrcAddrInc > 0 )
			{
				if ( burststart || MRAMBurstTable[MRAMBurstCount] == 0 )
				{
					MRAMBurstCount = 0;

					if ( dst_rgn == Mem7.GBAROM || dst_rgn == Mem7.Wifi0 || dst_rgn == Mem7.Wifi1 )
						MRAMBurstTable = dst_s == 8 ? DMATiming.MRAMRead32Bursts[2] : DMATiming.MRAMRead32Bursts[3];
					else if ( dst_n == 2 )
						MRAMBurstTable = DMATiming.MRAMRead32Bursts[0];
					else
						MRAMBurstTable = DMATiming.MRAMRead32Bursts[1];
				}

				return MRAMBurstTable[MRAMBurstCount++];
			}

			uint rbase = ((CurSrcAddr & 0x1F) == 0x1C) ? (dst_n == 2 ? 7u : 8u) : 9u;
			return rbase + (burststart ? dst_n : dst_s);
		}

		if ( dst_rgn == Mem7.MainRAM )
		{
			if ( DstAddrInc > 0 )
			{
				if ( burststart || MRAMBurstTable[MRAMBurstCount] == 0 )
				{
					MRAMBurstCount = 0;

					if ( src_rgn == Mem7.GBAROM || src_rgn == Mem7.Wifi0 || src_rgn == Mem7.Wifi1 )
						MRAMBurstTable = src_s == 8 ? DMATiming.MRAMWrite32Bursts[2] : DMATiming.MRAMWrite32Bursts[3];
					else if ( src_n == 2 )
						MRAMBurstTable = DMATiming.MRAMWrite32Bursts[0];
					else
						MRAMBurstTable = DMATiming.MRAMWrite32Bursts[1];
				}

				return MRAMBurstTable[MRAMBurstCount++];
			}

			return (burststart ? src_n : src_s) + 8;
		}

		if ( (src_rgn & dst_rgn) != 0 )
			return src_n + dst_n + 1;

		return burststart ? src_n + dst_n : src_s + dst_s;
	}

	private void Run9()
	{
		if ( NDS.ARM9Timestamp >= NDS.ARM9Target ) return;

		Executing = true;

		bool burststart = Running == 2;
		Running = 1;

		if ( (Cnt & (1 << 26)) == 0 )
		{
			while ( IterCount > 0 && !Stall )
			{
				NDS.ARM9Timestamp += UnitTimings9_16( burststart ) << NDS.ARM9ClockShift;
				burststart = false;

				NDS.ARM9Write16( CurDstAddr, NDS.ARM9Read16( CurSrcAddr ) );

				CurSrcAddr = (uint)(CurSrcAddr + (SrcAddrInc << 1));
				CurDstAddr = (uint)(CurDstAddr + (DstAddrInc << 1));
				IterCount--;
				RemCount--;

				if ( NDS.ARM9Timestamp >= NDS.ARM9Target ) break;
			}
		}
		else
		{
			while ( IterCount > 0 && !Stall )
			{
				NDS.ARM9Timestamp += UnitTimings9_32( burststart ) << NDS.ARM9ClockShift;
				burststart = false;

				NDS.ARM9Write32( CurDstAddr, NDS.ARM9Read32( CurSrcAddr ) );

				CurSrcAddr = (uint)(CurSrcAddr + (SrcAddrInc << 2));
				CurDstAddr = (uint)(CurDstAddr + (DstAddrInc << 2));
				IterCount--;
				RemCount--;

				if ( NDS.ARM9Timestamp >= NDS.ARM9Target ) break;
			}
		}

		Executing = false;
		Stall = false;

		if ( RemCount != 0 )
		{
			if ( IterCount == 0 )
			{
				Running = 0;
				NDS.ResumeCPU( 0, 1u << (int)Num );

				if ( StartMode == 0x07 )
					NDS.GPU3D.CheckFIFODMA();
			}

			return;
		}

		if ( (Cnt & (1 << 25)) == 0 )
			Cnt &= ~(1u << 31);

		if ( (Cnt & (1 << 30)) != 0 )
			NDS.SetIRQ( 0, IRQ.DMA0 + (int)Num );

		Running = 0;
		InProgress = false;
		NDS.ResumeCPU( 0, 1u << (int)Num );

		if ( StartMode == 0x05 )
			NDS.CartCheckDMA();
	}

	private void Run7()
	{
		if ( NDS.ARM7Timestamp >= NDS.ARM7Target ) return;

		Executing = true;

		bool burststart = Running == 2;
		Running = 1;

		if ( (Cnt & (1 << 26)) == 0 )
		{
			while ( IterCount > 0 && !Stall )
			{
				NDS.ARM7Timestamp += UnitTimings7_16( burststart );
				burststart = false;

				NDS.ARM7Write16( CurDstAddr, NDS.ARM7Read16( CurSrcAddr ) );

				CurSrcAddr = (uint)(CurSrcAddr + (SrcAddrInc << 1));
				CurDstAddr = (uint)(CurDstAddr + (DstAddrInc << 1));
				IterCount--;
				RemCount--;

				if ( NDS.ARM7Timestamp >= NDS.ARM7Target ) break;
			}
		}
		else
		{
			while ( IterCount > 0 && !Stall )
			{
				NDS.ARM7Timestamp += UnitTimings7_32( burststart );
				burststart = false;

				NDS.ARM7Write32( CurDstAddr, NDS.ARM7Read32( CurSrcAddr ) );

				CurSrcAddr = (uint)(CurSrcAddr + (SrcAddrInc << 2));
				CurDstAddr = (uint)(CurDstAddr + (DstAddrInc << 2));
				IterCount--;
				RemCount--;

				if ( NDS.ARM7Timestamp >= NDS.ARM7Target ) break;
			}
		}

		Executing = false;
		Stall = false;

		if ( RemCount != 0 )
		{
			if ( IterCount == 0 )
			{
				Running = 0;
				NDS.ResumeCPU( 1, 1u << (int)Num );
			}

			return;
		}

		if ( (Cnt & (1 << 25)) == 0 )
			Cnt &= ~(1u << 31);

		if ( (Cnt & (1 << 30)) != 0 )
			NDS.SetIRQ( 1, IRQ.DMA0 + (int)Num );

		Running = 0;
		InProgress = false;
		NDS.ResumeCPU( 1, 1u << (int)Num );

		if ( StartMode == 0x12 )
			NDS.CartCheckDMA();
	}
}