From 143bd655c6cfc50ea2878f41b024e9bf160806cd Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Fri, 10 Aug 2018 18:03:19 -0700 Subject: [PATCH] Making progress on PPU implementation --- app/src/main/kotlin/android/emu6502/CPU.kt | 2 +- .../android/emu6502/ExtensionFunctions.kt | 9 + .../kotlin/android/emu6502/nes/Controller.kt | 8 + .../android/emu6502/nes/INESFileHeader.kt | 2 - .../android/emu6502/nes/INESFileParser.kt | 51 +++-- .../main/kotlin/android/emu6502/nes/PPU.kt | 214 ++++++++++++++++-- .../android/emu6502/nes/mappers/MMC3.kt | 53 +++-- .../android/emu6502/nes/mappers/Mapper.kt | 2 +- 8 files changed, 263 insertions(+), 78 deletions(-) create mode 100644 app/src/main/kotlin/android/emu6502/ExtensionFunctions.kt diff --git a/app/src/main/kotlin/android/emu6502/CPU.kt b/app/src/main/kotlin/android/emu6502/CPU.kt index 460b6eb..862fc1c 100644 --- a/app/src/main/kotlin/android/emu6502/CPU.kt +++ b/app/src/main/kotlin/android/emu6502/CPU.kt @@ -109,7 +109,7 @@ class CPU(val memory: Memory) : Display.Callbacks { target.method.invoke() } else { val candidate = Opcodes.MAP.entries - .first { it.value.any { it.opcode == instruction } } + .first { e -> e.value.any { it.opcode == instruction } } throw Exception( "Address $${PC.toHexString()} - unknown opcode 0x${instruction.toHexString()} " + "(instruction ${candidate.key.name})") diff --git a/app/src/main/kotlin/android/emu6502/ExtensionFunctions.kt b/app/src/main/kotlin/android/emu6502/ExtensionFunctions.kt new file mode 100644 index 0000000..afd8bf0 --- /dev/null +++ b/app/src/main/kotlin/android/emu6502/ExtensionFunctions.kt @@ -0,0 +1,9 @@ +package android.emu6502 + +infix fun Byte.shr(other: Int): Byte { + return this.toInt().shr(other).toByte() +} + +infix fun Byte.shl(other: Int): Byte { + return this.toInt().shr(other).toByte() +} \ No newline at end of file diff --git a/app/src/main/kotlin/android/emu6502/nes/Controller.kt b/app/src/main/kotlin/android/emu6502/nes/Controller.kt index b357b93..7d0dec9 100644 --- a/app/src/main/kotlin/android/emu6502/nes/Controller.kt +++ b/app/src/main/kotlin/android/emu6502/nes/Controller.kt @@ -1,5 +1,13 @@ package android.emu6502.nes class Controller { + var buttons: Array = Array(8) { false } + var index: Byte = 0 + var strobe: Byte = 0 + fun read() { + } + + fun write() { + } } \ No newline at end of file diff --git a/app/src/main/kotlin/android/emu6502/nes/INESFileHeader.kt b/app/src/main/kotlin/android/emu6502/nes/INESFileHeader.kt index e43a071..226ace2 100644 --- a/app/src/main/kotlin/android/emu6502/nes/INESFileHeader.kt +++ b/app/src/main/kotlin/android/emu6502/nes/INESFileHeader.kt @@ -8,7 +8,6 @@ import java.util.* // Sample header: // 4e 45 53 1a 10 10 40 00 00 00 00 00 00 00 00 00 data class INESFileHeader( - // @formatter:off val magic: ByteArray, // Constant $4E $45 $53 $1A ("NES" followed by MS-DOS end-of-file) val numPRG: Byte, // Size of PRG ROM in 16 KB units val numCHR: Byte, // Size of CHR ROM in 8 KB units (Value 0 means the board uses CHR RAM) @@ -16,7 +15,6 @@ data class INESFileHeader( val control2: Byte, // Flags 7 val numRAM: Byte, // Size of PRG RAM in 8 KB units val padding: ByteArray // 7 bytes, unused - // @formatter:on ) { fun isValid() = Arrays.equals(magic, INES_FILE_MAGIC) && Arrays.equals(padding, PADDING) diff --git a/app/src/main/kotlin/android/emu6502/nes/INESFileParser.kt b/app/src/main/kotlin/android/emu6502/nes/INESFileParser.kt index b5f679d..ef6f3d1 100644 --- a/app/src/main/kotlin/android/emu6502/nes/INESFileParser.kt +++ b/app/src/main/kotlin/android/emu6502/nes/INESFileParser.kt @@ -21,30 +21,31 @@ internal class INESFileParser { (0..6).map { dataStream.readByte() }.toByteArray()) } - internal fun parseCartridge(file: File): Cartridge { - val stream = file.inputStream() - stream.use { - val inesFileHeader = parseFileHeader(stream) - // mapper type - val control1 = inesFileHeader.control1.toInt() - val mapper1 = control1 shr 4 - val mapper2 = inesFileHeader.control2.toInt() shr 4 - val mapper = mapper1 or (mapper2 shl 4) - // mirroring type - val mirror1 = control1 and 1 - val mirror2 = (control1 shr 3) and 1 - val mirror = mirror1 or (mirror2 shl 1) - // battery-backed RAM - val battery = control1.shr(1).and(1).toByte() - // read prg-rom bank(s) - val pgr = ByteArray(inesFileHeader.numPRG.toInt() * 16384) - stream.read(pgr) - // read chr-rom bank(s) - val chr = ByteArray(inesFileHeader.numCHR.toInt() * 8192) - stream.read(chr) - return Cartridge(pgr.map(Byte::toInt).toIntArray(), chr.map(Byte::toInt).toIntArray(), - mapper.toByte(), mirror, battery) - } - } + internal fun parseCartridge(file: File): Cartridge = + file.inputStream().use { + val inesFileHeader = parseFileHeader(it) + // mapper type + val control1 = inesFileHeader.control1.toInt() + val mapper1 = control1 shr 4 + val mapper2 = inesFileHeader.control2.toInt() shr 4 + val mapper = mapper1 or (mapper2 shl 4) + // mirroring type + val mirror1 = control1 and 1 + val mirror2 = (control1 shr 3) and 1 + val mirror = mirror1 or (mirror2 shl 1) + // battery-backed RAM + val battery = (control1 shr 1).and(1).toByte() + // read prg-rom bank(s) + val prg = ByteArray(inesFileHeader.numPRG.toInt() * 16384) + it.read(prg) + // read chr-rom bank(s) + val chr = ByteArray(inesFileHeader.numCHR.toInt() * 8192) + it.read(chr) + return Cartridge( + prg.map(Byte::toInt).toIntArray(), + chr.map(Byte::toInt).toIntArray(), + mapper.toByte(), mirror, battery + ) + } } } \ No newline at end of file diff --git a/app/src/main/kotlin/android/emu6502/nes/PPU.kt b/app/src/main/kotlin/android/emu6502/nes/PPU.kt index 28f194a..25374c7 100644 --- a/app/src/main/kotlin/android/emu6502/nes/PPU.kt +++ b/app/src/main/kotlin/android/emu6502/nes/PPU.kt @@ -1,47 +1,211 @@ package android.emu6502.nes +import android.emu6502.shl +import android.emu6502.shr +import android.media.Image +import android.system.Os.read +import kotlin.experimental.and +import kotlin.experimental.xor + class PPU( + val console: Console, // @formatter:off - val cycle: Int = 0, // 0-340 - val scanLine: Int = 0, // 0-261, 0-239=visible, 240=post, 241-260=vblank, 261=pre - val frame: Int = 0, // frame counter + var cycle: Int = 0, // 0-340 + var scanLine: Int = 0, // 0-261, 0-239=visible, 240=post, 241-260=vblank, 261=pre + var frame: Int = 0, // frame counter + + // storage variables + var paletteData: ByteArray = ByteArray(32), + var nameTableData: ByteArray = ByteArray(2048), + var oamData: ByteArray = ByteArray(256), + var front: Image, + var back: Image, // PPU registers - val v: Int = 0, // current vram address (15 bit) - val t: Int = 0, // temporary vram address (15 bit) - val x: Byte = 0, // fine x scroll (3 bit) - val w: Byte = 0, // write toggle (1 bit) - val f: Byte = 0, // even/odd frame flag (1 bit) - val register: Byte = 0, + var v: Int = 0, // current vram address (15 bit) + var t: Int = 0, // temporary vram address (15 bit) + var x: Byte = 0, // fine x scroll (3 bit) + var w: Byte = 0, // write toggle (1 bit) + var f: Byte = 0, // even/odd frame flag (1 bit) + var register: Byte = 0, + + var nmiOccurred: Boolean = false, + var nmiOutput: Boolean = false, + var nmiPrevious: Boolean = false, + var nmiDelay: Byte = 0, + + // background temporary variables + var nameTableByte: Byte = 0, + var attributeTableByte: Byte = 0, + var lowTileByte: Byte = 0, + var highTileByte: Byte = 0, + var tileData: Int = 0, // $2000 PPUCTRL - val flagNameTable: Boolean = false, // 0: $2000; 1: $2400; 2: $2800; 3: $2C00 - val flagIncrement: Boolean = false, // 0: add 1; 1: add 32 - val flagSpriteTable: Boolean = false, // 0: $0000; 1: $1000; ignored in 8x16 mode - val flagBackgroundTable: Boolean = false, // 0: $0000; 1: $1000 - val flagSpriteSize: Boolean = false, // 0: 8x8; 1: 8x16 - val flagMasterSlave: Boolean = false, // 0: read EXT; 1: write EXT + var flagNameTable: Byte = 0, // 0: $2000; 1: $2400; 2: $2800; 3: $2C00 + var flagIncrement: Byte = 0, // 0: add 1; 1: add 32 + var flagSpriteTable: Byte = 0, // 0: $0000; 1: $1000; ignored in 8x16 mode + var flagBackgroundTable: Byte = 0, // 0: $0000; 1: $1000 + var flagSpriteSize: Byte = 0, // 0: 8x8; 1: 8x16 + var flagMasterSlave: Byte = 0, // 0: read EXT; 1: write EXT // $2001 PPUMASK - val flagGrayscale: Boolean = false, // 0: color; 1: grayscale - val flagShowLeftBackground: Boolean = false, // 0: hide; 1: show - val flagShowLeftSprites: Boolean = false, // 0: hide; 1: show - val flagShowBackground: Boolean = false, // 0: hide; 1: show - val flagShowSprites: Boolean = false, // 0: hide; 1: show - val flagRedTint: Boolean = false, // 0: normal; 1: emphasized - val flagGreenTint: Boolean = false, // 0: normal; 1: emphasized - val flagBlueTint: Boolean = false, // 0: normal; 1: emphasized + var flagGrayscale: Byte = 0, // 0: color; 1: grayscale + var flagShowLeftBackground: Byte = 0, // 0: hide; 1: show + var flagShowLeftSprites: Byte = 0, // 0: hide; 1: show + var flagShowBackground: Byte = 0, // 0: hide; 1: show + var flagShowSprites: Byte = 0, // 0: hide; 1: show + var flagRedTint: Byte = 0, // 0: normal; 1: emphasized + var flagGreenTint: Byte = 0, // 0: normal; 1: emphasized + var flagBlueTint: Byte = 0, // 0: normal; 1: emphasized // $2002 PPUSTATUS val flagSpriteZeroHit: Boolean = false, val flagSpriteOverflow: Boolean = false, // $2003 OAMADDR - val oamAddress: Byte = 0, + var oamAddress: Byte = 0, // $2007 PPUDATA val bufferedData: Byte = 0 // for buffered reads // @formatter:on -) \ No newline at end of file +) { + fun writeRegister(address: Int, value: Byte) { + register = value + when (address) { + 0x2000 -> writeControl(value) + 0x2001 -> writeMask(value) + 0x2003 -> writeOAMAddress(value) + 0x2004 -> writeOAMData(value) + 0x2005 -> writeScroll(value) + 0x2006 -> writeAddress(value) + 0x2007 -> writeData(value) + 0x4014 -> writeDMA(value) + } + } + + private fun writeDMA(value: Byte) { + TODO() + } + + private fun step() { + tick() + val renderingEnabled = flagShowBackground != 0.toByte() || flagShowSprites != 0.toByte() + val preLine = scanLine == 261 + val visibleLine = scanLine < 240 + // postLine = scanLine == 240 + val renderLine = preLine || visibleLine + val preFetchCycle = cycle in 321..336 + val visibleCycle = cycle in 1..256 + val fetchCycle = preFetchCycle || visibleCycle + } + + private fun tick() { + if (nmiDelay > 0) { + nmiDelay-- + if (nmiDelay == 0.toByte() && nmiOutput && nmiOccurred) { + console.cpu.triggerNMI() + } + } + + if (flagShowBackground != 0.toByte() || flagShowSprites != 0.toByte()) { + if (f == 1.toByte() && scanLine == 261 && cycle == 339) { + cycle = 0 + scanLine = 0 + frame++ + f = f xor 1 + return + } + } + cycle++ + if (cycle > 340) { + cycle = 0 + scanLine++ + if (scanLine > 261) { + scanLine = 0 + frame++ + f = f xor 1 + } + } + } + + private fun writeData(value: Byte) { + TODO("not implemented") + } + + // $2006: PPUADDR + private fun writeAddress(value: Byte) { + if (w == 0.toByte()) { + // t: ..FEDCBA ........ = d: ..FEDCBA + // t: .X...... ........ = 0 + // w: = 1 + t = (t and 0x80FF) or ((value and 0x3F) shl 8).toInt() + w = 1 + } else { + // t: ........ HGFEDCBA = d: HGFEDCBA + // v = t + // w: = 0 + t = (t and 0xFF00) or value.toInt() + v = t + w = 0 + } + } + + // $2005: PPUSCROLL + private fun writeScroll(value: Byte) { + TODO("not implemented") + } + + // $2004: OAMDATA (write) + private fun writeOAMData(value: Byte) { + oamData[oamAddress.toInt()] = value + oamAddress++ + } + + // $2003: OAMADDR + private fun writeOAMAddress(value: Byte) { + oamAddress = value + } + + private fun writeMask(value: Byte) { + flagGrayscale = (value shr 0) and 1 + flagShowLeftBackground = (value shr 1) and 1 + flagShowLeftSprites = (value shr 2) and 1 + flagShowBackground = (value shr 3) and 1 + flagShowSprites = (value shr 4) and 1 + flagRedTint = (value shr 5) and 1 + flagGreenTint = (value shr 6) and 1 + flagBlueTint = (value shr 7) and 1 + } + + // $2000: PPUCTRL + private fun writeControl(value: Byte) { + flagNameTable = (value shr 0) and 3 + flagIncrement = (value shr 2) and 1 + flagSpriteTable = (value shr 3) and 1 + flagBackgroundTable = (value shr 4) and 1 + flagSpriteSize = (value shr 5) and 1 + flagMasterSlave = (value shr 6) and 1 + nmiOutput = (value shr 7) and 1 == 1.toByte() + nmiChange() + // t: ....BA.. ........ = d: ......BA + t = (t and 0xF3FF) or ((value and 0x03) shl 10).toInt() + } + + private fun nmiChange() { + val nmi = nmiOutput && nmiOccurred + if (nmi && !nmiPrevious) { + // TODO: this fixes some games but the delay shouldn't have to be so + // long, so the timings are off somewhere + nmiDelay = 15 + } + nmiPrevious = nmi + } + + private fun fetchNameTableByte() { + val address = 0x2000 or (v and 0x0FFF) + nameTableByte = read(address) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/android/emu6502/nes/mappers/MMC3.kt b/app/src/main/kotlin/android/emu6502/nes/mappers/MMC3.kt index c3dc05a..cd9e344 100644 --- a/app/src/main/kotlin/android/emu6502/nes/mappers/MMC3.kt +++ b/app/src/main/kotlin/android/emu6502/nes/mappers/MMC3.kt @@ -22,34 +22,39 @@ class MMC3( private var irqEnable: Boolean = false override fun read(address: Int): Int { - if (address < 0x2000) { - val bank = address / 0x0400 - val offset = address % 0x0400 - return cartridge.chr[chrOffsets[bank] + offset] + return when { + address < 0x2000 -> { + val bank = address / 0x0400 + val offset = address % 0x0400 + cartridge.chr[chrOffsets[bank] + offset] + } + address >= 0x8000 -> { + val addr = address - 0x8000 + val bank = addr / 0x2000 + val offset = addr % 0x2000 + cartridge.pgr[prgOffsets[bank] + offset] + } + address >= 0x6000 -> { + return cartridge.sram[address - 0x6000] + } + else -> throw RuntimeException("unhandled mapper4 read at address: ${address.toHexString()}") } - if (address >= 0x8000) { - val addr = address - 0x8000 - val bank = addr / 0x2000 - val offset = addr % 0x2000 - return cartridge.pgr[prgOffsets[bank] + offset] - } - if (address >= 0x6000) { - return cartridge.sram[address - 0x6000] - } - throw RuntimeException("unhandled mapper4 read at address: ${address.toHexString()}") } override fun write(address: Int, value: Int) { - if (address < 0x2000) { - val bank = address / 0x0400 - val offset = address % 0x0400 - cartridge.chr[chrOffsets[bank] + offset] = value - } else if (address >= 0x8000) { - writeRegister(address, value) - } else if (address >= 0x6000) { - cartridge.sram[address - 0x6000] = value - } else { - throw RuntimeException("unhandled mapper4 write at address ${address.toHexString()}") + when { + address < 0x2000 -> { + val bank = address / 0x0400 + val offset = address % 0x0400 + cartridge.chr[chrOffsets[bank] + offset] = value + } + address >= 0x8000 -> { + writeRegister(address, value) + } + address >= 0x6000 -> { + cartridge.sram[address - 0x6000] = value + } + else -> throw RuntimeException("unhandled mapper4 write at address ${address.toHexString()}") } } diff --git a/app/src/main/kotlin/android/emu6502/nes/mappers/Mapper.kt b/app/src/main/kotlin/android/emu6502/nes/mappers/Mapper.kt index 10ca569..abce61a 100644 --- a/app/src/main/kotlin/android/emu6502/nes/mappers/Mapper.kt +++ b/app/src/main/kotlin/android/emu6502/nes/mappers/Mapper.kt @@ -13,7 +13,7 @@ interface Mapper { fun newMapper(cartridge: Cartridge, ppu: PPU, cpu: CPU): Mapper = when (cartridge.mapper.toInt()) { 4 -> MMC3(cartridge, ppu, cpu) - else -> throw NotImplementedError() + else -> throw NotImplementedError("Mapper ${cartridge.mapper.toInt()} not implemented") } } } \ No newline at end of file