using static OptimeGBA.Bits; using System; namespace OptimeGBA { public enum DmaStartTimingGba { Immediately = 0, VBlank = 1, HBlank = 2, Special = 3, } public sealed class DmaChannelGba { public uint DMASAD; public uint DMADAD; public uint DMACNT_L; public uint DmaSource; public uint DmaDest; public uint DmaLength; // DMACNT_H public DmaDestAddrCtrl DestAddrCtrl; public DmaSrcAddrCtrl SrcAddrCtrl; public bool Repeat; public bool TransferType; public bool GamePakDRQ; public DmaStartTimingGba StartTiming; public bool FinishedIRQ; public bool Enabled; // Don't directly set to false, use Disable() public uint DMACNT_H; public byte ReadHwio8(uint addr) { // DMASAD, DMADAD, and DMACNT_L are write-only byte val = 0; switch (addr) { case 0x0A: // DMACNT_H B0 case 0x0B: // DMACNT_H B1 val = GetByteIn(GetControl(), addr & 1); break; } return val; } public void WriteHwio8(uint addr, byte val) { switch (addr) { case 0x00: // DMASAD B0 case 0x01: // DMASAD B1 case 0x02: // DMASAD B2 case 0x03: // DMASAD B3 DMASAD = SetByteIn(DMASAD, val, addr & 3); break; case 0x04: // DMADAD B0 case 0x05: // DMADAD B1 case 0x06: // DMADAD B2 case 0x07: // DMADAD B3 DMADAD = SetByteIn(DMADAD, val, addr & 3); break; case 0x08: // DMACNT_L B0 case 0x09: // DMACNT_L B1 DMACNT_L = SetByteIn(DMACNT_L, val, addr & 1); break; case 0x0A: // DMACNT_H B0 case 0x0B: // DMACNT_H B1 DMACNT_H = SetByteIn(DMACNT_H, val, addr & 1); UpdateControl(); break; } } public void UpdateControl() { DestAddrCtrl = (DmaDestAddrCtrl)BitRange(DMACNT_H, 5, 6); SrcAddrCtrl = (DmaSrcAddrCtrl)BitRange(DMACNT_H, 7, 8); Repeat = BitTest(DMACNT_H, 9); TransferType = BitTest(DMACNT_H, 10); GamePakDRQ = BitTest(DMACNT_H, 11); StartTiming = (DmaStartTimingGba)BitRange(DMACNT_H, 12, 13); FinishedIRQ = BitTest(DMACNT_H, 14); if (BitTest(DMACNT_H, 15)) { Enable(); } else { Disable(); } } public uint GetControl() { uint val = 0; val |= ((uint)DestAddrCtrl & 0b11) << 5; val |= ((uint)SrcAddrCtrl & 0b11) << 7; if (Repeat) val = BitSet(val, 9); if (TransferType) val = BitSet(val, 10); if (GamePakDRQ) val = BitSet(val, 11); val |= ((uint)StartTiming & 0b11) << 12; if (FinishedIRQ) val = BitSet(val, 14); if (Enabled) val = BitSet(val, 15); DMACNT_H = val; return val; } public void Enable() { if (!Enabled) { DmaSource = DMASAD; DmaDest = DMADAD; DmaLength = DMACNT_L; } Enabled = true; GetControl(); } public void Disable() { Enabled = false; GetControl(); } } public unsafe sealed class DmaGba { Gba Gba; public DmaChannelGba[] Ch = new DmaChannelGba[4] { new DmaChannelGba(), new DmaChannelGba(), new DmaChannelGba(), new DmaChannelGba(), }; static readonly uint[] DmaSourceMask = { 0x07FFFFFF, 0x0FFFFFFF, 0x0FFFFFFF, 0x0FFFFFFF }; static readonly uint[] DmaDestMask = { 0x07FFFFFF, 0x07FFFFFF, 0x07FFFFFF, 0x0FFFFFFFF }; public bool DmaLock; public DmaGba(Gba gba) { Gba = gba; } public byte ReadHwio8(uint addr) { if (addr >= 0x40000B0 && addr <= 0x40000BB) { return Ch[0].ReadHwio8(addr - 0x40000B0); } else if (addr >= 0x40000BC && addr <= 0x40000C7) { return Ch[1].ReadHwio8(addr - 0x40000BC); } else if (addr >= 0x40000C8 && addr <= 0x40000D3) { return Ch[2].ReadHwio8(addr - 0x40000C8); } else if (addr >= 0x40000D4 && addr <= 0x40000DF) { return Ch[3].ReadHwio8(addr - 0x40000D4); } throw new Exception("This shouldn't happen."); } public void WriteHwio8(uint addr, byte val) { if (addr >= 0x40000B0 && addr <= 0x40000BB) { bool oldEnabled = Ch[0].Enabled; Ch[0].WriteHwio8(addr - 0x40000B0, val); if (!oldEnabled && Ch[0].Enabled) ExecuteImmediate(0); return; } else if (addr >= 0x40000BC && addr <= 0x40000C7) { bool oldEnabled = Ch[1].Enabled; Ch[1].WriteHwio8(addr - 0x40000BC, val); if (!oldEnabled && Ch[1].Enabled) ExecuteImmediate(1); return; } else if (addr >= 0x40000C8 && addr <= 0x40000D3) { bool oldEnabled = Ch[2].Enabled; Ch[2].WriteHwio8(addr - 0x40000C8, val); if (!oldEnabled && Ch[2].Enabled) ExecuteImmediate(2); return; } else if (addr >= 0x40000D4 && addr <= 0x40000DF) { bool oldEnabled = Ch[3].Enabled; Ch[3].WriteHwio8(addr - 0x40000D4, val); if (!oldEnabled && Ch[3].Enabled) ExecuteImmediate(3); return; } throw new Exception("This shouldn't happen."); } public void ExecuteDma(DmaChannelGba c, uint ci) { DmaLock = true; // Least significant 28 (or 27????) bits c.DmaSource &= DmaSourceMask[ci]; c.DmaDest &= DmaDestMask[ci]; if (ci == 3) { // DMA 3 is 16-bit length c.DmaLength &= 0b1111111111111111; // Value of zero is treated as maximum length if (c.DmaLength == 0) c.DmaLength = 0x10000; } else { // DMA 0-2 are 14-bit length c.DmaLength &= 0b11111111111111; // Value of zero is treated as maximum length if (c.DmaLength == 0) c.DmaLength = 0x4000; } // Console.WriteLine($"Starting DMA {ci}"); // Console.WriteLine($"SRC: {Util.HexN(srcAddr, 7)}"); // Console.WriteLine($"DEST: {Util.HexN(destAddr, 7)}"); // Console.WriteLine($"LENGTH: {Util.HexN(c.DmaLength, 4)}"); int destOffsPerUnit; int sourceOffsPerUnit; if (c.TransferType) { switch (c.DestAddrCtrl) { case DmaDestAddrCtrl.Increment: destOffsPerUnit = +4; break; case DmaDestAddrCtrl.Decrement: destOffsPerUnit = -4; break; case DmaDestAddrCtrl.IncrementReload: destOffsPerUnit = +4; break; default: destOffsPerUnit = 0; break; } switch (c.SrcAddrCtrl) { case DmaSrcAddrCtrl.Increment: sourceOffsPerUnit = +4; break; case DmaSrcAddrCtrl.Decrement: sourceOffsPerUnit = -4; break; default: sourceOffsPerUnit = 0; break; } } else { switch (c.DestAddrCtrl) { case DmaDestAddrCtrl.Increment: destOffsPerUnit = +2; break; case DmaDestAddrCtrl.Decrement: destOffsPerUnit = -2; break; case DmaDestAddrCtrl.IncrementReload: destOffsPerUnit = +2; break; default: destOffsPerUnit = 0; break; } switch (c.SrcAddrCtrl) { case DmaSrcAddrCtrl.Increment: sourceOffsPerUnit = +2; break; case DmaSrcAddrCtrl.Decrement: sourceOffsPerUnit = -2; break; default: sourceOffsPerUnit = 0; break; } } uint origLength = c.DmaLength; if (c.TransferType) { for (; c.DmaLength > 0; c.DmaLength--) { Gba.Mem.Write32(c.DmaDest & ~3u, Gba.Mem.Read32(c.DmaSource & ~3u)); Gba.Tick(Gba.Cpu.Timing32[(c.DmaSource >> 24) & 0xF]); Gba.Tick(Gba.Cpu.Timing32[(c.DmaDest >> 24) & 0xF]); c.DmaDest = (uint)(long)(destOffsPerUnit + c.DmaDest); c.DmaSource = (uint)(long)(sourceOffsPerUnit + c.DmaSource); } } else { for (; c.DmaLength > 0; c.DmaLength--) { Gba.Mem.Write16(c.DmaDest & ~1u, Gba.Mem.Read16(c.DmaSource & ~1u)); Gba.Tick(Gba.Cpu.Timing8And16[(c.DmaSource >> 24) & 0xF]); Gba.Tick(Gba.Cpu.Timing8And16[(c.DmaDest >> 24) & 0xF]); c.DmaDest = (uint)(long)(destOffsPerUnit + c.DmaDest); c.DmaSource = (uint)(long)(sourceOffsPerUnit + c.DmaSource); } } if (c.DestAddrCtrl == DmaDestAddrCtrl.IncrementReload) { c.DmaLength = origLength; if (c.Repeat) { c.DmaDest = c.DMADAD; } } if (c.FinishedIRQ) { Gba.HwControl.FlagInterrupt((uint)InterruptGba.Dma0 + ci); } DmaLock = false; } public void ExecuteSoundDma(DmaChannelGba c, uint ci) { DmaLock = true; // Least significant 28 (or 27????) bits uint srcAddr = c.DmaSource & 0b1111111111111111111111111111; uint destAddr = c.DmaDest & 0b111111111111111111111111111; // 4 units of 32bits (16 bytes) are transferred to FIFO_A or FIFO_B for (uint i = 0; i < 4; i++) { uint val = Gba.Mem.Read32(srcAddr + 0); if (destAddr == 0x40000A0) { Gba.GbaAudio.A.Insert((byte)val); Gba.GbaAudio.A.Insert((byte)(val >>= 8)); Gba.GbaAudio.A.Insert((byte)(val >>= 8)); Gba.GbaAudio.A.Insert((byte)(val >>= 8)); } else if (destAddr == 0x40000A4) { Gba.GbaAudio.B.Insert((byte)val); Gba.GbaAudio.B.Insert((byte)(val >>= 8)); Gba.GbaAudio.B.Insert((byte)(val >>= 8)); Gba.GbaAudio.B.Insert((byte)(val >>= 8)); } else { Gba.Mem.Write8(destAddr + 0, (byte)val); Gba.Mem.Write8(destAddr + 1, (byte)(val >>= 8)); Gba.Mem.Write8(destAddr + 2, (byte)(val >>= 8)); Gba.Mem.Write8(destAddr + 3, (byte)(val >>= 8)); } switch (c.SrcAddrCtrl) { case DmaSrcAddrCtrl.Increment: srcAddr += 4; break; case DmaSrcAddrCtrl.Decrement: srcAddr -= 4; break; case DmaSrcAddrCtrl.Fixed: break; } // Applying proper timing to sound DMAs causes crackling in certain games including PMD. // This only happens with scheduled timers, which leads me to believe the real problem is in there. // PROBLEM SOLVED.... my timers were 1 cycle too slow to reload Gba.Cpu.InstructionCycles += (Gba.Cpu.Timing32[(c.DmaSource >> 24) & 0xF]); Gba.Cpu.InstructionCycles += (Gba.Cpu.Timing32[(c.DmaDest >> 24) & 0xF]); } c.DmaSource = srcAddr; if (c.FinishedIRQ) { Gba.HwControl.FlagInterrupt((uint)InterruptGba.Dma0 + ci); } DmaLock = false; } public void ExecuteImmediate(uint ci) { DmaChannelGba c = Ch[ci]; if (c.Enabled && c.StartTiming == DmaStartTimingGba.Immediately) { c.Disable(); ExecuteDma(c, ci); } } public void RepeatFifoA() { if (!DmaLock) { if (Ch[1].StartTiming == DmaStartTimingGba.Special) { ExecuteSoundDma(Ch[1], 1); } } } public void RepeatFifoB() { if (!DmaLock) { if (Ch[2].StartTiming == DmaStartTimingGba.Special) { ExecuteSoundDma(Ch[2], 2); } } } public void RepeatHblank() { if (!DmaLock) { for (uint ci = 0; ci < 4; ci++) { DmaChannelGba c = Ch[ci]; if (c.StartTiming == DmaStartTimingGba.HBlank) { c.DmaLength = c.DMACNT_L; ExecuteDma(c, ci); } } } } public void RepeatVblank() { if (!DmaLock) { for (uint ci = 0; ci < 4; ci++) { DmaChannelGba c = Ch[ci]; if (c.StartTiming == DmaStartTimingGba.VBlank) { c.DmaLength = c.DMACNT_L; ExecuteDma(c, ci); } } } } } }