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 }