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 }