1 module gbaid.gba.dma; 2 3 import std.meta : AliasSeq; 4 5 import gbaid.util; 6 7 import gbaid.gba.io; 8 import gbaid.gba.memory; 9 import gbaid.gba.interrupt; 10 import gbaid.gba.halt; 11 12 public class DMAs { 13 private MemoryBus* memory; 14 private InterruptHandler interruptHandler; 15 private HaltHandler haltHandler; 16 mixin declareFields!(int, true, "srcAddress", 0, 4); 17 mixin declareFields!(int, true, "destAddress", 0, 4); 18 mixin declareFields!(int, true, "_wordCount", 0, 4); 19 mixin declareFields!(int, true, "control", 0, 4); 20 mixin declareFields!(Timing, true, "timing", Timing.DISABLED, 4); 21 mixin declareFields!(int, true, "internSrcAddress", 0, 4); 22 mixin declareFields!(int, true, "internDestAddress", 0, 4); 23 mixin declareFields!(int, true, "internWordCount", 0, 4); 24 private int triggered = 0; 25 26 public this(MemoryBus* memory, IoRegisters* ioRegisters, InterruptHandler interruptHandler, HaltHandler haltHandler) { 27 this.memory = memory; 28 this.interruptHandler = interruptHandler; 29 this.haltHandler = haltHandler; 30 31 ioRegisters.mapAddress(0xB0, &srcAddress!0, 0x07FFFFFF, 0); 32 ioRegisters.mapAddress(0xB4, &destAddress!0, 0x07FFFFFF, 0).postWriteMonitor(&onDestPostWrite!0); 33 ioRegisters.mapAddress(0xB8, &_wordCount!0, 0x3FFF, 0); 34 ioRegisters.mapAddress(0xB8, &control!0, 0xFFF0, 16).postWriteMonitor(&onControlPostWrite!0); 35 36 ioRegisters.mapAddress(0xBC, &srcAddress!1, 0x0FFFFFFF, 0); 37 ioRegisters.mapAddress(0xC0, &destAddress!1, 0x07FFFFFF, 0).postWriteMonitor(&onDestPostWrite!1); 38 ioRegisters.mapAddress(0xC4, &_wordCount!1, 0x3FFF, 0); 39 ioRegisters.mapAddress(0xC4, &control!1, 0xFFF0, 16).postWriteMonitor(&onControlPostWrite!1); 40 41 ioRegisters.mapAddress(0xC8, &srcAddress!2, 0x0FFFFFFF, 0); 42 ioRegisters.mapAddress(0xCC, &destAddress!2, 0x07FFFFFF, 0).postWriteMonitor(&onDestPostWrite!2); 43 ioRegisters.mapAddress(0xD0, &_wordCount!2, 0x3FFF, 0); 44 ioRegisters.mapAddress(0xD0, &control!2, 0xFFF0, 16).postWriteMonitor(&onControlPostWrite!2); 45 46 ioRegisters.mapAddress(0xD4, &srcAddress!3, 0x0FFFFFFF, 0); 47 ioRegisters.mapAddress(0xD8, &destAddress!3, 0x0FFFFFFF, 0).postWriteMonitor(&onDestPostWrite!3); 48 ioRegisters.mapAddress(0xDC, &_wordCount!3, 0xFFFF, 0); 49 ioRegisters.mapAddress(0xDC, &control!3, 0xFFF0, 16).postWriteMonitor(&onControlPostWrite!3); 50 } 51 52 public alias signalVBLANK = triggerDMAs!(Timing.VBLANK); 53 public alias signalHBLANK = triggerDMAs!(Timing.HBLANK); 54 55 public alias signalSoundQueueA = triggerDMAs!(Timing.SOUND_QUEUE_A); 56 public alias signalSoundQueueB = triggerDMAs!(Timing.SOUND_QUEUE_B); 57 58 private void onDestPostWrite(int channel)(int mask, int oldDest, int newDest) { 59 // Update the timing (this is a special case for SOUND_QUEUE_X timings, which depend on the destination) 60 updateTiming!channel(); 61 } 62 63 private void onControlPostWrite(int channel)(int mask, int oldControl, int newControl) { 64 updateTiming!channel(); 65 // If the DMA enable bit goes high, reload the addresses and word count, and signal the immediate timing 66 if (mask.checkBit(15) && !oldControl.checkBit(15) && newControl.checkBit(15)) { 67 internSrcAddress!channel = srcAddress!channel; 68 internDestAddress!channel = destAddress!channel; 69 internWordCount!channel = wordCount!channel; 70 triggerDMAs!(Timing.IMMEDIATE); 71 } 72 } 73 74 private void updateTiming(int channel)() { 75 if (!control!channel.checkBit(15)) { 76 timing!channel = Timing.DISABLED; 77 return; 78 } 79 final switch (control!channel.getBits(12, 13)) { 80 case 0: 81 timing!channel = Timing.IMMEDIATE; 82 break; 83 case 1: 84 timing!channel = Timing.VBLANK; 85 break; 86 case 2: 87 timing!channel = Timing.HBLANK; 88 break; 89 case 3: { 90 static if (channel == 1 || channel == 2) { 91 switch (destAddress!channel) { 92 case 0x40000A0: 93 timing!channel = Timing.SOUND_QUEUE_A; 94 break; 95 case 0x40000A4: 96 timing!channel = Timing.SOUND_QUEUE_B; 97 break; 98 default: 99 timing!channel = Timing.DISABLED; 100 } 101 } else static if (channel == 3) { 102 timing!channel = Timing.VIDEO_CAPTURE; 103 } else { 104 throw new Error("Can't use special DMA timing for channel 0"); 105 } 106 } 107 } 108 } 109 110 @property 111 private int wordCount(int channel)() { 112 if (control!channel.getBits(12, 13) == 3) { 113 static if (channel == 1 || channel == 2) { 114 return 0x4; 115 } else static if (channel == 3) { 116 // TODO: implement video capture 117 throw new Error("Unimplemented: video capture DMAs"); 118 } else { 119 throw new Error("Can't use special DMA timing for channel 0"); 120 } 121 } 122 if (_wordCount!channel == 0) { 123 static if (channel == 3) { 124 return 0x10000; 125 } else { 126 return 0x4000; 127 } 128 } 129 return _wordCount!channel; 130 } 131 132 private void triggerDMAs(Timing trigger)() { 133 foreach (channel; AliasSeq!(0, 1, 2, 3)) { 134 if (timing!channel == trigger) { 135 triggered.setBit(channel, 1); 136 } 137 } 138 // Stop the CPU if any transfer has been started 139 haltHandler.dmaHalt(triggered != 0); 140 } 141 142 public size_t emulate(size_t cycles) { 143 // Check if any of the DMAs are triggered 144 if (triggered != 0) { 145 // Run the DMAs with respect to priority 146 foreach (channel; AliasSeq!(0, 1, 2, 3)) { 147 if (updateChannel!channel(cycles)) { 148 static if (channel == 3) { 149 // Out of DMAs to run, waste all the cycles left 150 cycles = 0; 151 } 152 } else { 153 break; 154 } 155 } 156 } else { 157 // If not then discard all the cycles 158 cycles = 0; 159 } 160 // Restart the CPU if all transfers are complete 161 haltHandler.dmaHalt(triggered != 0); 162 return cycles; 163 } 164 165 private bool updateChannel(int channel)(ref size_t cycles) { 166 // Only run if triggered 167 if (!triggered.checkBit(channel)) { 168 // No transfer to do 169 return true; 170 } 171 // Use 3 cycles per word and for the finalization step 172 while (cycles >= 3) { 173 // Take the cycles 174 cycles -= 3; 175 if (internWordCount!channel > 0) { 176 // Copy a single word 177 copyWord!channel(); 178 } else { 179 // Finalize the DMA when the transfer is complete 180 finalizeDMA!channel(); 181 // The transfer is complete 182 return true; 183 } 184 } 185 // The transfer is incomplete because we ran out of cycles 186 return false; 187 } 188 189 private void finalizeDMA(int channel)() { 190 int dmaAddress = channel * 0xC + 0xB8; 191 if (control!channel.checkBit(9)) { 192 // Repeating DMA, reload the word count 193 internWordCount!channel = wordCount!channel; 194 if (control!channel.getBits(5, 6) == 3) { 195 // We also reload the destination address, and we must also check for timing changes 196 internDestAddress!channel = destAddress!channel; 197 updateTiming!channel(); 198 } 199 // Clear the trigger is the DMA timing isn't immediate 200 if (timing!channel != Timing.IMMEDIATE) { 201 triggered.setBit(channel, 0); 202 } 203 } else { 204 // Clear the DMA enable bit 205 control!channel.setBit(15, 0); 206 timing!channel = Timing.DISABLED; 207 // Always clear the trigger for single-run DMAs 208 triggered.setBit(channel, 0); 209 } 210 // Trigger DMA end interrupt if enabled 211 if (control!channel.checkBit(14)) { 212 interruptHandler.requestInterrupt(InterruptSource.DMA_0 + channel); 213 } 214 } 215 216 private void copyWord(int channel)() { 217 int type = void; 218 int srcAddressControl = control!channel.getBits(7, 8); 219 int destAddressControl = void; 220 switch (timing!channel) with (Timing) { 221 case DISABLED: 222 throw new Error("DMA channel is disabled"); 223 case SOUND_QUEUE_A: 224 case SOUND_QUEUE_B: 225 type = 1; 226 destAddressControl = 2; 227 break; 228 case VIDEO_CAPTURE: 229 // TODO: implement video capture 230 throw new Error("Unimplemented: video capture DMAs"); 231 default: 232 type = control!channel.getBit(10); 233 destAddressControl = control!channel.getBits(5, 6); 234 } 235 int increment = type ? 4 : 2; 236 237 if (type) { 238 memory.set!int(internDestAddress!channel, memory.get!int(internSrcAddress!channel)); 239 } else { 240 memory.set!short(internDestAddress!channel, memory.get!short(internSrcAddress!channel)); 241 } 242 243 internSrcAddress!channel.modifyAddress(srcAddressControl, increment); 244 internDestAddress!channel.modifyAddress(destAddressControl, increment); 245 internWordCount!channel--; 246 } 247 } 248 249 private void modifyAddress(ref int address, int control, int amount) { 250 final switch (control) { 251 case 0: 252 case 3: 253 address += amount; 254 break; 255 case 1: 256 address -= amount; 257 break; 258 case 2: 259 break; 260 } 261 } 262 263 private enum Timing { 264 DISABLED, 265 IMMEDIATE, 266 VBLANK, 267 HBLANK, 268 SOUND_QUEUE_A, 269 SOUND_QUEUE_B, 270 VIDEO_CAPTURE 271 }