PPU: Implement registers

This commit is contained in:
Andrew Glaze 2024-08-19 11:21:24 -04:00
parent 9c5cb22223
commit 05927d8e91
13 changed files with 371 additions and 8 deletions

View File

@ -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)]
}
}

107
Sources/PPU/NesPPU.swift Normal file
View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}
}

View File