1 module nes.mapper4;
2 
3 import std.conv;
4 import std.format;
5 
6 import nes.cartridge;
7 import nes.console;
8 import nes.cpu;
9 import nes.mapper;
10 import nes.memory;
11 
12 class Mapper4 : Mapper {
13     this(Console console, Cartridge cartridge) {
14         this.console = console;
15         this.cart = cartridge;
16 
17         this.prgOffsets[0] = this.prgBankOffset(0);
18         this.prgOffsets[1] = this.prgBankOffset(1);
19         this.prgOffsets[2] = this.prgBankOffset(-2);
20         this.prgOffsets[3] = this.prgBankOffset(-1);
21     }
22 
23     void step() {
24         auto ppu = this.console.ppu;
25         if (ppu.cycle != 280) { // TODO: this *should* be 260
26             return;
27         }
28         if (ppu.scanLine > 239 && ppu.scanLine < 261) {
29             return;
30         }
31         if (ppu.flagShowBackground == 0 && ppu.flagShowSprites == 0) {
32             return;
33         }
34         this.handleScanLine();
35     }
36 
37     void handleScanLine() {
38         if (this.counter == 0) {
39             this.counter = this.reload;
40         } else {
41             this.counter--;
42             if (this.counter == 0 && this.irqEnable) {
43                 this.console.cpu.addIrqSource(IrqSource.External);
44             }
45         }
46     }
47 
48     ubyte read(ushort address) {
49         if (address < 0x2000) {
50             auto bank = address / 0x0400;
51             auto offset = address % 0x0400;
52             return this.cart.chr[this.chrOffsets[bank] + cast(int)offset];
53         }
54         else if (address >= 0x8000) {
55             address = cast(ushort)(address - 0x8000);
56             auto bank = address / 0x2000;
57             auto offset = address % 0x2000;
58             return this.cart.prg[this.prgOffsets[bank] + cast(int)offset];
59         }
60         else if (address >= 0x6000) {
61             auto index = cast(int)address - 0x6000;
62             return this.cart.sram[index];
63         }
64         else {
65             throw new MapperException(format("unhandled mapper4 read at address: 0x%04X", address));
66         }
67     }
68 
69     void write(ushort address, ubyte value) {
70         if (address < 0x2000) {
71             auto bank = address / 0x0400;
72             auto offset = address % 0x0400;
73             this.cart.chr[this.chrOffsets[bank] + cast(int)offset] = value;
74         }
75         else if (address >= 0x8000) {
76             this.writeRegister(address, value);
77         }
78         else if (address >= 0x6000) {
79             auto index = cast(int)address - 0x6000;
80             this.cart.sram[index] = value;
81         }
82         else {
83             throw new MapperException(format("unhandled mapper4 write at address: 0x%04X", address));
84         }
85     }
86 
87     void save(string[string] state) {
88         state["mapper4.register"] = to!string(this.register);
89         state["mapper4.registers"] = to!string(this.registers);
90         state["mapper4.prgMode"] = to!string(this.prgMode);
91         state["mapper4.chrMode"] = to!string(this.chrMode);
92         state["mapper4.prgOffsets"] = to!string(this.prgOffsets);
93         state["mapper4.chrOffsets"] = to!string(this.chrOffsets);
94         state["mapper4.reload"] = to!string(this.reload);
95         state["mapper4.counter"] = to!string(this.counter);
96         state["mapper4.irqEnable"] = to!string(this.irqEnable);
97     }
98 
99     void load(string[string] state) {
100         this.register = to!ubyte(state["mapper4.register"]);
101         this.registers = to!(ubyte[8])(state["mapper4.registers"]);
102         this.prgMode = to!ubyte(state["mapper4.prgMode"]);
103         this.chrMode = to!ubyte(state["mapper4.chrMode"]);
104         this.prgOffsets = to!(int[4])(state["mapper4.prgOffsets"]);
105         this.chrOffsets = to!(int[8])(state["mapper4.chrOffsets"]);
106         this.reload = to!ubyte(state["mapper4.reload"]);
107         this.counter = to!ubyte(state["mapper4.counter"]);
108         this.irqEnable = to!bool(state["mapper4.irqEnable"]);
109     }
110 
111     private:
112         Cartridge cart;
113         Console   console;
114         ubyte     register;
115         ubyte[8]  registers;
116         ubyte     prgMode;
117         ubyte     chrMode;
118         int[4]    prgOffsets;
119         int[8]    chrOffsets;
120         ubyte     reload;
121         ubyte     counter;
122         bool      irqEnable;
123 
124         void writeRegister(ushort address, ubyte value) {
125             if (address <= 0x9FFF && (address % 2) == 0)
126                 this.writeBankSelect(value);
127             else if (address <= 0x9FFF && (address % 2) == 1)
128                 this.writeBankData(value);
129             else if (address <= 0xBFFF && (address % 2) == 0)
130                 this.writeMirror(value);
131             else if (address <= 0xBFFF && (address % 2) == 1)
132                 this.writeProtect(value);
133             else if (address <= 0xDFFF && (address % 2) == 0)
134                 this.writeIRQLatch(value);
135             else if (address <= 0xDFFF && (address % 2) == 1)
136                 this.writeIRQReload(value);
137             else if (address <= 0xFFFF && (address % 2) == 0)
138                 this.writeIRQDisable(value);
139             else if (address <= 0xFFFF && (address % 2) == 1)
140                 this.writeIRQEnable(value);
141         }
142 
143         void writeBankSelect(ubyte value) {
144             this.prgMode = (value >> 6) & 1;
145             this.chrMode = (value >> 7) & 1;
146             this.register = value & 7;
147             this.updateOffsets();
148         }
149 
150         void writeBankData(ubyte value) {
151             this.registers[this.register] = value;
152             this.updateOffsets();
153         }
154 
155         void writeMirror(ubyte value) {
156             switch (value & 1) {
157                 case 0:
158                     this.cart.mirror = MirrorVertical;
159                     break;
160                 case 1:
161                     this.cart.mirror = MirrorHorizontal;
162                     break;
163                 default:
164                     break;
165             }
166         }
167 
168         void writeProtect(ubyte value) {
169         }
170 
171         void writeIRQLatch(ubyte value) {
172             this.reload = value;
173         }
174 
175         void writeIRQReload(ubyte value) {
176             this.counter = 0;
177         }
178 
179         void writeIRQDisable(ubyte value) {
180             this.irqEnable = false;
181             this.console.cpu.clearIrqSource(IrqSource.External);
182         }
183 
184         void writeIRQEnable(ubyte value) {
185             this.irqEnable = true;
186         }
187 
188         int prgBankOffset(int index) {
189             if (index >= 0x80) {
190                 index -= 0x100;
191             }
192             index %= cast(int)(this.cart.prg.length / 0x2000);
193             int offset = index * 0x2000;
194             if (offset < 0) {
195                 offset += this.cart.prg.length;
196             }
197             return offset;
198         }
199 
200         int chrBankOffset(int index) {
201             if (index >= 0x80) {
202                 index -= 0x100;
203             }
204             index %= cast(int)(this.cart.chr.length / 0x0400);
205             int offset = index * 0x0400;
206             if (offset < 0) {
207                 offset += this.cart.chr.length;
208             }
209             return offset;
210         }
211 
212         void updateOffsets() {
213             switch (this.prgMode) {
214                 case 0:
215                     this.prgOffsets[0] = this.prgBankOffset(cast(int)this.registers[6]);
216                     this.prgOffsets[1] = this.prgBankOffset(cast(int)this.registers[7]);
217                     this.prgOffsets[2] = this.prgBankOffset(-2);
218                     this.prgOffsets[3] = this.prgBankOffset(-1);
219                     break;
220                 case 1:
221                     this.prgOffsets[0] = this.prgBankOffset(-2);
222                     this.prgOffsets[1] = this.prgBankOffset(cast(int)this.registers[7]);
223                     this.prgOffsets[2] = this.prgBankOffset(cast(int)this.registers[6]);
224                     this.prgOffsets[3] = this.prgBankOffset(-1);
225                     break;
226                 default:
227                     break;
228             }
229             switch (this.chrMode) {
230                 case 0:
231                     this.chrOffsets[0] = this.chrBankOffset(cast(int)(this.registers[0] & 0xFE));
232                     this.chrOffsets[1] = this.chrBankOffset(cast(int)(this.registers[0] | 0x01));
233                     this.chrOffsets[2] = this.chrBankOffset(cast(int)(this.registers[1] & 0xFE));
234                     this.chrOffsets[3] = this.chrBankOffset(cast(int)(this.registers[1] | 0x01));
235                     this.chrOffsets[4] = this.chrBankOffset(cast(int)this.registers[2]);
236                     this.chrOffsets[5] = this.chrBankOffset(cast(int)this.registers[3]);
237                     this.chrOffsets[6] = this.chrBankOffset(cast(int)this.registers[4]);
238                     this.chrOffsets[7] = this.chrBankOffset(cast(int)this.registers[5]);
239                     break;
240                 case 1:
241                     this.chrOffsets[0] = this.chrBankOffset(cast(int)this.registers[2]);
242                     this.chrOffsets[1] = this.chrBankOffset(cast(int)this.registers[3]);
243                     this.chrOffsets[2] = this.chrBankOffset(cast(int)this.registers[4]);
244                     this.chrOffsets[3] = this.chrBankOffset(cast(int)this.registers[5]);
245                     this.chrOffsets[4] = this.chrBankOffset(cast(int)(this.registers[0] & 0xFE));
246                     this.chrOffsets[5] = this.chrBankOffset(cast(int)(this.registers[0] | 0x01));
247                     this.chrOffsets[6] = this.chrBankOffset(cast(int)(this.registers[1] & 0xFE));
248                     this.chrOffsets[7] = this.chrBankOffset(cast(int)(this.registers[1] | 0x01));
249                     break;
250                 default:
251                     break;
252             }
253         }
254 }