From 05927d8e9122cfab85602ec7f0f5b299cabbce68 Mon Sep 17 00:00:00 2001 From: Andrew Glaze Date: Mon, 19 Aug 2024 11:21:24 -0400 Subject: [PATCH] PPU: Implement registers --- Sources/{ => CPU}/CPU.swift | 0 Sources/{ => CPU}/CPU_Instructions.swift | 0 Sources/{ => CPU}/Trace.swift | 0 Sources/{ => CPU}/opcodes.swift | 0 Sources/{ => Data}/Bus.swift | 44 ++++++-- Sources/{ => Data}/Rom.swift | 0 Sources/PPU/NesPPU.swift | 107 ++++++++++++++++++++ Sources/PPU/Registers/AddrRegister.swift | 42 ++++++++ Sources/PPU/Registers/ControlRegister.swift | 37 +++++++ Sources/PPU/Registers/MaskRegister.swift | 58 +++++++++++ Sources/PPU/Registers/ScrollRegister.swift | 18 ++++ Sources/PPU/Registers/StatusRegister.swift | 73 +++++++++++++ Sources/Util.swift | 0 13 files changed, 371 insertions(+), 8 deletions(-) rename Sources/{ => CPU}/CPU.swift (100%) rename Sources/{ => CPU}/CPU_Instructions.swift (100%) rename Sources/{ => CPU}/Trace.swift (100%) rename Sources/{ => CPU}/opcodes.swift (100%) rename Sources/{ => Data}/Bus.swift (52%) rename Sources/{ => Data}/Rom.swift (100%) create mode 100644 Sources/PPU/NesPPU.swift create mode 100644 Sources/PPU/Registers/AddrRegister.swift create mode 100644 Sources/PPU/Registers/ControlRegister.swift create mode 100644 Sources/PPU/Registers/MaskRegister.swift create mode 100644 Sources/PPU/Registers/ScrollRegister.swift create mode 100644 Sources/PPU/Registers/StatusRegister.swift delete mode 100644 Sources/Util.swift diff --git a/Sources/CPU.swift b/Sources/CPU/CPU.swift similarity index 100% rename from Sources/CPU.swift rename to Sources/CPU/CPU.swift diff --git a/Sources/CPU_Instructions.swift b/Sources/CPU/CPU_Instructions.swift similarity index 100% rename from Sources/CPU_Instructions.swift rename to Sources/CPU/CPU_Instructions.swift diff --git a/Sources/Trace.swift b/Sources/CPU/Trace.swift similarity index 100% rename from Sources/Trace.swift rename to Sources/CPU/Trace.swift diff --git a/Sources/opcodes.swift b/Sources/CPU/opcodes.swift similarity index 100% rename from Sources/opcodes.swift rename to Sources/CPU/opcodes.swift diff --git a/Sources/Bus.swift b/Sources/Data/Bus.swift similarity index 52% rename from Sources/Bus.swift rename to Sources/Data/Bus.swift index 14e132c..1e0d2a3 100644 --- a/Sources/Bus.swift +++ b/Sources/Data/Bus.swift @@ -1,6 +1,7 @@ class Bus { var cpuVram: [UInt8] = .init(repeating: 0, count: 2048) - var rom: Rom + var prgRom: [UInt8] + var ppu: NesPPU fileprivate let RAM : UInt16 = 0x0000 fileprivate let RAM_MIRRORS_END: UInt16 = 0x1FFF @@ -10,7 +11,8 @@ class Bus { fileprivate let ROM_ADDRESS_END: UInt16 = 0xFFFF init(_ rom: Rom) { - self.rom = rom + ppu = NesPPU(rom.character, rom.screenMirror) + self.prgRom = rom.program } } @@ -21,9 +23,17 @@ extension Bus: Memory { case RAM...RAM_MIRRORS_END: let mirrorDownAddr = addr & 0b00000111_11111111 return self.cpuVram[Int(mirrorDownAddr)] - case PPU_REGISTERS...PPU_REGISTERS_MIRRORS_END: + case 0x2000, 0x2001, 0x2003, 0x2005, 0x2006, 0x4014: + fatalError("Attempt to read from write-only PPU address \(addr)") + case 0x2002: + return ppu.readStatus() + case 0x2004: + return ppu.readOamData() + case 0x2007: + return ppu.readData() + case 0x2008...PPU_REGISTERS_MIRRORS_END: let mirrorDownAddr = addr & 0b00100000_00000111; - fatalError("PPU not implemented yet") + return self.memRead(mirrorDownAddr) case ROM_ADDRESS_START...ROM_ADDRESS_END: return readProgramRom(addr) default: @@ -37,9 +47,27 @@ extension Bus: Memory { case RAM...RAM_MIRRORS_END: let mirrorDownAddr = addr & 0b11111111111 self.cpuVram[Int(mirrorDownAddr)] = data - case PPU_REGISTERS...PPU_REGISTERS_MIRRORS_END: + case 0x2000: + ppu.writeToCtrl(data) + case 0x2001: + ppu.writeToMask(data) + case 0x2002: + fatalError("Attempt to write to PPU status register") + case 0x2003: + ppu.writeToOamAddr(data) + case 0x2004: + ppu.writeToOamData(data) + case 0x2005: + ppu.writeToScroll(data) + case 0x2006: + ppu.writeToPPUAddr(data) + case 0x2007: + ppu.writeToData(data) + case 0x2008...PPU_REGISTERS_MIRRORS_END: let mirrorDownAddr = addr & 0b00100000_00000111 - fatalError("PPU is not implemented yet!") + memWrite(mirrorDownAddr, data: data) + case ROM_ADDRESS_START...ROM_ADDRESS_END: + fatalError("Attempt to write to Cartridge ROM space: \(addr)") default: print("Ignorming mem-write at \(addr)") } @@ -47,10 +75,10 @@ extension Bus: Memory { func readProgramRom(_ addr: UInt16) -> UInt8 { var addr = addr - 0x8000 - if rom.program.count == 0x4000 && addr >= 0x4000 { + if prgRom.count == 0x4000 && addr >= 0x4000 { // rom mirroring addr = addr % 0x4000 } - return rom.program[Int(addr)] + return prgRom[Int(addr)] } } diff --git a/Sources/Rom.swift b/Sources/Data/Rom.swift similarity index 100% rename from Sources/Rom.swift rename to Sources/Data/Rom.swift diff --git a/Sources/PPU/NesPPU.swift b/Sources/PPU/NesPPU.swift new file mode 100644 index 0000000..1367f76 --- /dev/null +++ b/Sources/PPU/NesPPU.swift @@ -0,0 +1,107 @@ +class NesPPU { + public var paletteTable = [UInt8](repeating: 0, count: 32) + public var vram = [UInt8](repeating: 0, count: 2048) + + public let mirroring: Mirroring + public var chrRom: [UInt8] + + private let addr: AddrRegister = AddrRegister() + private var readBuff: UInt8 = 0 + var ctrl = ControlRegister() + let status = StatusRegister() + let scroll = ScrollRegister() + let mask = MaskRegister() + + var oamAddr: UInt8 = 0 + var oamData = [UInt8](repeating: 0, count: 64 * 4) + + init(_ chrRom: [UInt8], _ mirroring: Mirroring) { + self.chrRom = chrRom + self.mirroring = mirroring + } + + func writeToPPUAddr(_ value: UInt8) { + addr.update(value) + } + + func writeToCtrl(_ value: UInt8) { + ctrl.rawValue = value + } + + func incrememtVramAddr() { + addr.increment(ctrl.vramAddrIncrement()) + } + + func readData() -> UInt8 { + let addr = addr.get() + incrememtVramAddr() + + switch addr { + case 0...0x1fff: + let res = readBuff + readBuff = chrRom[Int(addr)] + return res + case 0x2000...0x2fff: + let res = readBuff + readBuff = vram[Int(mirrorVramAddr(addr))] + return res + case 0x3000...0x3eff: + fatalError("addr space 0x3000..0x3eff is not expected to be used, requested = \(addr)") + case 0x3f00...0x3fff: + return self.paletteTable[Int(addr - 0x3f00)] + default: + fatalError("Unexpected access to mirrored space \(addr)") + } + } + + func mirrorVramAddr(_ addr: UInt16) -> UInt16 { + let mirroredVram = addr & 0b10111111111111 // mirror down 0x3000-0x3eff to 0x2000 - 0x2eff + let vramIndex = mirroredVram - 0x2000 // to vram array index + let nameTable = vramIndex / 0x400 // to the name index table + return switch (mirroring, nameTable) { + case (.vertical, 2), (.vertical, 3): + vramIndex - 0x800 + case (.horizontal, 2): + vramIndex - 0x400 + case (.horizontal, 1): + vramIndex - 0x400 + case (.horizontal, 3): + vramIndex - 0x800 + default: + vramIndex + } + } + + func writeToData(_ data: UInt8) { + fatalError("Not Implemented") + } + + func readStatus() -> UInt8 { + let data = status.snapshot() + status.resetVblankStatus() + addr.resetLatch() + scroll.resetLatch() + return data + } + + func writeToScroll(_ value: UInt8) { + scroll.write(value) + } + + func writeToOamAddr(_ value: UInt8) { + oamAddr = value + } + + func writeToOamData(_ value: UInt8) { + oamData[Int(oamAddr)] = value + oamAddr = oamAddr &+ 1 + } + + func readOamData() -> UInt8 { + oamData[Int(oamAddr)] + } + + func writeToMask(_ value: UInt8) { + mask.update(value) + } +} diff --git a/Sources/PPU/Registers/AddrRegister.swift b/Sources/PPU/Registers/AddrRegister.swift new file mode 100644 index 0000000..b4ade58 --- /dev/null +++ b/Sources/PPU/Registers/AddrRegister.swift @@ -0,0 +1,42 @@ +class AddrRegister { + private var value: (UInt8, UInt8) = (0, 0) // hi byte first, lo byte second + private var hiPointer = true + + func set(_ data: UInt16) { + value.0 = UInt8(data >> 8) + value.1 = UInt8(data & 0xff) + } + + func update(_ data: UInt8) { + if hiPointer { + value.0 = data + } else { + value.1 = data + } + + if get() > 0x3fff { + set(get() & 0b11111111111111) + } + hiPointer.toggle() + } + + func increment(_ inc: UInt8) { + let lo = value.1 + value.1 = value.1 &+ inc + + if lo > value.1 { + value.0 = value.0 &+ 1 + } + if get() > 0x3fff { + set(get() & 0b11111111111111) + } + } + + func resetLatch() { + hiPointer = true + } + + func get() -> UInt16 { + UInt16(value.0) << 8 | UInt16(value.1) + } +} diff --git a/Sources/PPU/Registers/ControlRegister.swift b/Sources/PPU/Registers/ControlRegister.swift new file mode 100644 index 0000000..c0ec2c5 --- /dev/null +++ b/Sources/PPU/Registers/ControlRegister.swift @@ -0,0 +1,37 @@ +// 7 bit 0 +// ---- ---- +// VPHB SINN +// |||| |||| +// |||| ||++- Base nametable address +// |||| || (0 = $2000; 1 = $2400; 2 = $2800; 3 = $2C00) +// |||| |+--- VRAM address increment per CPU read/write of PPUDATA +// |||| | (0: add 1, going across; 1: add 32, going down) +// |||| +---- Sprite pattern table address for 8x8 sprites +// |||| (0: $0000; 1: $1000; ignored in 8x16 mode) +// |||+------ Background pattern table address (0: $0000; 1: $1000) +// ||+------- Sprite size (0: 8x8 pixels; 1: 8x16 pixels) +// |+-------- PPU master/slave select +// | (0: read backdrop from EXT pins; 1: output color on EXT pins) +// +--------- Generate an NMI at the start of the +// vertical blanking interval (0: off; 1: on) + +struct ControlRegister: OptionSet { + var rawValue: UInt8 + + static let NAMETABLE1 = ControlRegister(rawValue: 0b00000001) + static let NAMETABLE2 = ControlRegister(rawValue: 0b00000010) + static let VRAM_ADD_INCREMENT = ControlRegister(rawValue: 0b00000100) + static let SPRITE_PATTERN_ADDR = ControlRegister(rawValue: 0b00001000) + static let BACKROUND_PATTERN_ADDR = ControlRegister(rawValue: 0b00010000) + static let SPRITE_SIZE = ControlRegister(rawValue: 0b00100000) + static let MASTER_SLAVE_SELECT = ControlRegister(rawValue: 0b01000000) + static let GENERATE_NMI = ControlRegister(rawValue: 0b10000000) + + func vramAddrIncrement() -> UInt8 { + if self.contains(.VRAM_ADD_INCREMENT) { + 1 + } else { + 32 + } + } +} diff --git a/Sources/PPU/Registers/MaskRegister.swift b/Sources/PPU/Registers/MaskRegister.swift new file mode 100644 index 0000000..52ceaa9 --- /dev/null +++ b/Sources/PPU/Registers/MaskRegister.swift @@ -0,0 +1,58 @@ +private struct MaskRegisterInternal: OptionSet { + var rawValue: UInt8 + + static let GREYSCALE = MaskRegisterInternal(rawValue: 0b00000001) + static let LEFTMOST_8PXL_BACKGROUND = MaskRegisterInternal(rawValue: 0b00000010) + static let LEFTMOST_8PXL_SPRITE = MaskRegisterInternal(rawValue: 0b00000100) + static let SHOW_BACKGROUND = MaskRegisterInternal(rawValue: 0b00001000) + static let SHOW_SPRITES = MaskRegisterInternal(rawValue: 0b00010000) + static let EMPHASISE_RED = MaskRegisterInternal(rawValue: 0b00100000) + static let EMPHASISE_GREEN = MaskRegisterInternal(rawValue: 0b01000000) + static let EMPHASISE_BLUE = MaskRegisterInternal(rawValue: 0b10000000) +} + +class MaskRegister { + private var val = MaskRegisterInternal() + + func isGrayscale() -> Bool { + val.contains(.GREYSCALE) + } + + func leftmost8pxlBackground() -> Bool { + val.contains(.LEFTMOST_8PXL_BACKGROUND) + } + + func leftmost8pxlSprite() -> Bool { + val.contains(.LEFTMOST_8PXL_SPRITE) + } + + func showBackground() -> Bool { + val.contains(.SHOW_BACKGROUND) + } + + func showSprites() -> Bool { + val.contains(.SHOW_SPRITES) + } + + func emphasise() -> [Color] { + var res: [Color] = [] + if val.contains(.EMPHASISE_RED) { + res.append(.Red) + } + if val.contains(.EMPHASISE_BLUE) { + res.append(.Blue) + } + if val.contains(.EMPHASISE_GREEN) { + res.append(.Green) + } + return res + } + + func update(_ data: UInt8) { + val.rawValue = data + } +} + +public enum Color { + case Red, Green, Blue +} diff --git a/Sources/PPU/Registers/ScrollRegister.swift b/Sources/PPU/Registers/ScrollRegister.swift new file mode 100644 index 0000000..f548305 --- /dev/null +++ b/Sources/PPU/Registers/ScrollRegister.swift @@ -0,0 +1,18 @@ +class ScrollRegister { + public var x: UInt8 = 0 + public var y: UInt8 = 0 + public var latch = false + + func write(_ data: UInt8) { + if !latch { + x = data + } else { + y = data + } + latch.toggle() + } + + func resetLatch() { + latch = false + } +} diff --git a/Sources/PPU/Registers/StatusRegister.swift b/Sources/PPU/Registers/StatusRegister.swift new file mode 100644 index 0000000..e9f9b39 --- /dev/null +++ b/Sources/PPU/Registers/StatusRegister.swift @@ -0,0 +1,73 @@ +// 7 bit 0 +// ---- ---- +// VSO. .... +// |||| |||| +// |||+-++++- Least significant bits previously written into a PPU register +// ||| (due to register not being updated for this address) +// ||+------- Sprite overflow. The intent was for this flag to be set +// || whenever more than eight sprites appear on a scanline, but a +// || hardware bug causes the actual behavior to be more complicated +// || and generate false positives as well as false negatives; see +// || PPU sprite evaluation. This flag is set during sprite +// || evaluation and cleared at dot 1 (the second dot) of the +// || pre-render line. +// |+-------- Sprite 0 Hit. Set when a nonzero pixel of sprite 0 overlaps +// | a nonzero background pixel; cleared at dot 1 of the pre-render +// | line. Used for raster timing. +// +--------- Vertical blank has started (0: not in vblank; 1: in vblank). +// Set at dot 1 of line 241 (the line *after* the post-render +// line); cleared after reading $2002 and at dot 1 of the +// pre-render line. + +private struct StatusRegisterInternal: OptionSet { + var rawValue: UInt8 + + static let NOTUSED = StatusRegisterInternal(rawValue: 0b00000001) + static let NOTUSED2 = StatusRegisterInternal(rawValue: 0b00000010) + static let NOTUSED3 = StatusRegisterInternal(rawValue: 0b00000100) + static let NOTUSED4 = StatusRegisterInternal(rawValue: 0b00001000) + static let NOTUSED5 = StatusRegisterInternal(rawValue: 0b00010000) + static let SPRITE_OVERFLOW = StatusRegisterInternal(rawValue: 0b00100000) + static let SPRITE_ZERO_HIT = StatusRegisterInternal(rawValue: 0b01000000) + static let VBLANK_STARTED = StatusRegisterInternal(rawValue: 0b10000000) +} + +class StatusRegister { + fileprivate var val = StatusRegisterInternal() + + func setVblankStatus(_ status: Bool) { + if status { + val.insert(.VBLANK_STARTED) + } else { + val.remove(.VBLANK_STARTED) + } + } + + func setSpriteZeroHit(_ status: Bool) { + if status { + val.insert(.SPRITE_ZERO_HIT) + } else { + val.remove(.SPRITE_ZERO_HIT) + } + } + + func setSpriteOverflow(_ status: Bool) { + if status { + val.insert(.SPRITE_OVERFLOW) + } else { + val.remove(.SPRITE_OVERFLOW) + } + } + + func resetVblankStatus() { + val.remove(.VBLANK_STARTED) + } + + func isInVblank() -> Bool { + val.contains(.VBLANK_STARTED) + } + + func snapshot() -> UInt8 { + val.rawValue + } +} diff --git a/Sources/Util.swift b/Sources/Util.swift deleted file mode 100644 index e69de29..0000000