Compare commits

...

10 Commits

Author SHA1 Message Date
Andrew Glaze
9f12bb2ba0 PPU: impl scrolling 2024-08-23 23:36:25 -04:00
Andrew Glaze
af3be8d93d fix cross-platform compatability 2024-08-22 21:45:47 -04:00
Andrew Glaze
df8be993f7 impl frame timing 2024-08-22 21:19:11 -04:00
Andrew Glaze
59e7559d77 Impl Joypad 2024-08-21 18:35:32 -04:00
Andrew Glaze
bd21449256 Render: don't render all sprites for every background tile 2024-08-21 17:45:32 -04:00
Andrew Glaze
6974110b22 Render: Impl sprite rendering 2024-08-21 16:42:05 -04:00
Andrew Glaze
ddd8fd6ee6 PPU: Impl color palettes 2024-08-21 10:13:15 -04:00
Andrew Glaze
f9efe23b0b PPU: background rendering 2024-08-20 23:36:04 -04:00
Andrew Glaze
be170bf750 remove large log file 2024-08-20 17:22:53 -04:00
1917d0f82e PPU: work on rendering 2024-08-20 17:21:52 -04:00
9 changed files with 388 additions and 127 deletions

View File

@@ -156,7 +156,7 @@ class CPU {
func run(onCycle: @escaping () -> (), onComplete: @escaping () -> ()) {
let opcodes = OPCODES_MAP
while true {
if let _nmi = bus.pollNMI() {
if bus.pollNMI() != nil {
interrupt(.NMI)
}
processOpcodes(onCycle: onCycle, opcodes: opcodes) {
@@ -194,7 +194,6 @@ class CPU {
let programCounterState = programCounter
guard let opcode = opcodes[code] else {fatalError("OpCode \(code) not recgonized!")}
// print(programCounter, opcode.mnemonic)
switch code {
/// LDA

View File

@@ -9,7 +9,7 @@ func dumpCpuState(_ cpu: CPU) -> String {
{
let (addr, _) = cpu.getAbsoluteAddress(opcode.mode, addr: cpu.programCounter + 1)
return (addr, cpu.memRead(addr))
return (addr, 0)//cpu.memRead(addr))
}()
}

View File

@@ -1,8 +1,10 @@
class Bus {
var cpuVram: [UInt8] = .init(repeating: 0, count: 2048)
var prgRom: [UInt8]
var ppu: NesPPU
let ppu: NesPPU
let joypad1: Joypad
var cycles: Int = 0
var gameloopCallback: (NesPPU) -> ()
fileprivate let RAM : UInt16 = 0x0000
fileprivate let RAM_MIRRORS_END: UInt16 = 0x1FFF
@@ -11,18 +13,28 @@ class Bus {
fileprivate let ROM_ADDRESS_START: UInt16 = 0x8000
fileprivate let ROM_ADDRESS_END: UInt16 = 0xFFFF
init(_ rom: Rom) {
init(rom: Rom, joypad1: Joypad, gameloopCallback: @escaping (NesPPU) -> ()) {
ppu = NesPPU(rom.character, rom.screenMirror)
self.prgRom = rom.program
self.gameloopCallback = gameloopCallback
self.joypad1 = joypad1
}
func tick(_ cycles: UInt8) {
self.cycles += Int(cycles)
self.ppu.tick(cycles * 3)
let nmiBefore = ppu.nmiInterrupt != nil
_ = self.ppu.tick(cycles * 3)
let nmiAfter = ppu.nmiInterrupt != nil
if !nmiBefore && nmiAfter {
gameloopCallback(ppu)
}
}
func pollNMI() -> UInt8? {
ppu.nmiInterrupt
ppu.pollNMI()
}
}
@@ -34,13 +46,20 @@ extension Bus: Memory {
let mirrorDownAddr = addr & 0b00000111_11111111
return self.cpuVram[Int(mirrorDownAddr)]
case 0x2000, 0x2001, 0x2003, 0x2005, 0x2006, 0x4014:
fatalError("Attempt to read from write-only PPU address \(addr)")
//fatalError("Attempt to read from write-only PPU address \(addr)")
return 0
case 0x2002:
return ppu.readStatus()
case 0x2004:
return ppu.readOamData()
case 0x2007:
return ppu.readData()
case 0x4000...0x4015:
return 0 // Ignore APU
case 0x4016:
return joypad1.read()
case 0x4017:
return 0 // Ignore Joy 2
case 0x2008...PPU_REGISTERS_MIRRORS_END:
let mirrorDownAddr = addr & 0b00100000_00000111;
return self.memRead(mirrorDownAddr)
@@ -76,6 +95,20 @@ extension Bus: Memory {
case 0x2008...PPU_REGISTERS_MIRRORS_END:
let mirrorDownAddr = addr & 0b00100000_00000111
memWrite(mirrorDownAddr, data: data)
case 0x4000...0x4013, 0x4015:
return // Ignore APU
case 0x4016:
joypad1.write(data)
case 0x4017:
return // Ignore Joy 2
case 0x4014:
var buffer = [UInt8](repeating: 0, count: 256)
let hi = UInt16(data) << 8
for i in 0..<256 {
buffer[i] = memRead(hi + UInt16(i))
}
ppu.writeOamDma(buffer)
case ROM_ADDRESS_START...ROM_ADDRESS_END:
fatalError("Attempt to write to Cartridge ROM space: \(addr)")
default:

44
Sources/Joypad.swift Normal file
View File

@@ -0,0 +1,44 @@
struct JoypadButton: OptionSet {
var rawValue: UInt8
static let RIGHT = JoypadButton(rawValue: 0b10000000)
static let LEFT = JoypadButton(rawValue: 0b01000000)
static let DOWN = JoypadButton(rawValue: 0b00100000)
static let UP = JoypadButton(rawValue: 0b00010000)
static let START = JoypadButton(rawValue: 0b00001000)
static let SELECT = JoypadButton(rawValue: 0b00000100)
static let BUTTON_B = JoypadButton(rawValue: 0b00000010)
static let BUTTON_A = JoypadButton(rawValue: 0b00000001)
}
class Joypad {
var strobe = false
var buttonIndex: UInt8 = 0
var buttonStatus = JoypadButton()
func write(_ data: UInt8) {
strobe = data & 1 == 1
if strobe {
buttonIndex = 0
}
}
func read() -> UInt8 {
if buttonIndex > 7 {
return 1
}
let response = (buttonStatus.rawValue & (1 << buttonIndex)) >> buttonIndex
if !strobe {
buttonIndex += 1
}
return response
}
func setButton(_ button: JoypadButton, pressed: Bool) {
if pressed {
buttonStatus.insert(button)
} else {
buttonStatus.remove(button)
}
}
}

View File

@@ -18,7 +18,7 @@ class NesPPU {
var scanline: UInt16 = 0
var cycles: Int = 0
var nmiInterrupt: UInt8?
var nmiInterrupt: UInt8? = nil
init(_ chrRom: [UInt8], _ mirroring: Mirroring) {
self.chrRom = chrRom
@@ -28,16 +28,17 @@ class NesPPU {
func tick(_ cycles: UInt8) -> Bool {
self.cycles += Int(cycles)
if self.cycles >= 341 {
if checkSprite0Hit(self.cycles) {
status.setSpriteZeroHit(true)
}
self.cycles = self.cycles - 341
scanline += 1
self.scanline += 1
if scanline == 241 {
if self.ctrl.generateVblankNMI() {
self.status.setVblankStatus(true)
status.setSpriteZeroHit(false)
if ctrl.generateVblankNMI() {
nmiInterrupt = 1
}
self.status.setVblankStatus(true)
status.setSpriteZeroHit(false)
if ctrl.generateVblankNMI() {
nmiInterrupt = 1
}
}
@@ -52,6 +53,18 @@ class NesPPU {
return false
}
func checkSprite0Hit(_ cycle: Int) -> Bool {
let y = Int(oamData[0])
let x = Int(oamData[0])
return (y == scanline) && x <= cycle && mask.showSprites()
}
func pollNMI() -> UInt8? {
let tmp = self.nmiInterrupt
self.nmiInterrupt = nil
return tmp
}
func writeToPPUAddr(_ value: UInt8) {
addr.update(value)
}
@@ -109,7 +122,31 @@ class NesPPU {
}
func writeToData(_ data: UInt8) {
fatalError("Not Implemented")
let addr = addr.get()
switch addr {
case 0...0x1fff:
print("Attempt to write to chr rom space \(addr)!")
case 0x2000...0x2fff:
self.vram[Int(mirrorVramAddr(addr))] = data
case 0x3000...0x3eff:
fatalError("addr \(addr) should not be used in reality!")
//Addresses $3F10/$3F14/$3F18/$3F1C are mirrors of $3F00/$3F04/$3F08/$3F0C
case 0x3f10, 0x3f14, 0x3f18, 0x3f1c:
let addrMirror = addr - 0x10
paletteTable[Int(addrMirror - 0x3f00)] = data
case 0x3f00...0x3fff:
paletteTable[Int(addr - 0x3f00)] = data
default:
fatalError("Unexpected access to mirrored space \(addr)")
}
incrememtVramAddr()
}
func writeOamDma(_ buffer: [UInt8]) {
for x in buffer {
oamData[Int(oamAddr)] = x
oamAddr = oamAddr &+ 1
}
}
func readStatus() -> UInt8 {

View File

@@ -28,7 +28,7 @@ struct ControlRegister: OptionSet {
static let GENERATE_NMI = ControlRegister(rawValue: 0b10000000)
func vramAddrIncrement() -> UInt8 {
if self.contains(.VRAM_ADD_INCREMENT) {
if !self.contains(.VRAM_ADD_INCREMENT) {
1
} else {
32
@@ -38,4 +38,51 @@ struct ControlRegister: OptionSet {
func generateVblankNMI() -> Bool {
self.contains(.GENERATE_NMI)
}
func backgroundPatternAddr() -> Int {
if !self.contains(.BACKROUND_PATTERN_ADDR) {
0
} else {
0x1000
}
}
func spritePatternAddr() -> Int {
if !self.contains(.SPRITE_PATTERN_ADDR) {
0
} else {
0x1000
}
}
func spriteSize() -> Int {
if !self.contains(.SPRITE_SIZE) {
8
} else {
16
}
}
func masterSlaveSelect() -> Int {
if !self.contains(.SPRITE_SIZE) {
0
} else {
1
}
}
func nametableAddr() -> UInt16 {
switch rawValue & 0b11 {
case 0:
0x2000
case 1:
0x2400
case 2:
0x2800
case 3:
0x2c00
default:
fatalError("naemtableAddr: Not possible!")
}
}
}

174
Sources/Render/Render.swift Normal file
View File

@@ -0,0 +1,174 @@
class Render {
static func render(_ ppu: NesPPU, frame: Frame) {
let scroll = (x: Int(ppu.scroll.x), y: Int(ppu.scroll.y))
let (mainNametable, secondNametable) = switch (ppu.mirroring, ppu.ctrl.nametableAddr()) {
case (.vertical, 0x2000), (.vertical, 0x2800), (.horizontal, 0x2000), (.horizontal, 0x2400):
(ppu.vram[0..<0x400], ppu.vram[0x400..<0x800])
case (.vertical, 0x2400), (.vertical, 0x2c00), (.horizontal, 0x2800), (.horizontal, 0x2c00):
(ppu.vram[0x400..<0x800], ppu.vram[0..<0x400])
default:
fatalError("cringe looking nametable arrangment: \(ppu.mirroring)")
}
renderNameTable(
ppu,
frame: frame,
nameTable: Array(mainNametable),
viewPort: Rect(x1: scroll.x, y1: scroll.y, x2: 256, y2: 240),
shift: (-scroll.x, -scroll.y)
)
if scroll.x > 0 {
renderNameTable(
ppu,
frame: frame,
nameTable: Array(secondNametable),
viewPort: Rect(x1: 0, y1: 0, x2: scroll.x, y2: 240),
shift: (256 - scroll.x, 0)
)
} else if scroll.y > 0 {
renderNameTable(
ppu,
frame: frame,
nameTable: Array(secondNametable),
viewPort: Rect(x1: 0, y1: 0, x2: 256, y2: scroll.y),
shift: (0, 240 - scroll.y)
)
}
// MARK: Draw Sprites
for i in stride(from: 0, to: ppu.oamData.count, by: 4) {
let tileIndex = UInt16(ppu.oamData[i + 1])
let tileX = Int(ppu.oamData[i + 3])
let tileY = Int(ppu.oamData[i])
let flipVert = ppu.oamData[i + 2] >> 7 & 1 == 1
let flipHori = ppu.oamData[i + 2] >> 6 & 1 == 1
let paletteIndex = ppu.oamData[i + 2] & 0b11
let spritePallete = getSpritePalette(ppu, paletteIndex: paletteIndex)
let bank = ppu.ctrl.spritePatternAddr()
let tile = ppu.chrRom[(bank + Int(tileIndex) * 16)...(bank + Int(tileIndex) * 16 + 15)]
for y in 0...7 {
var upper = tile[tile.startIndex + y]
var lower = tile[tile.startIndex + y + 8]
for x in [7,6,5,4,3,2,1,0] {
let value = (1 & lower) << 1 | (1 & upper)
upper = upper >> 1
lower = lower >> 1
if (value == 0) {
continue // skip coloring this pixel, it's transparent
}
let rgb = switch value {
case 1:
NESColor.SYSTEM_PALLETE[Int(spritePallete[1])]
case 2:
NESColor.SYSTEM_PALLETE[Int(spritePallete[2])]
case 3:
NESColor.SYSTEM_PALLETE[Int(spritePallete[3])]
default:
fatalError("Invalid Pallete Color type")
}
switch (flipHori, flipVert) {
case (false, false):
frame.setPixel((tileX + x, tileY + y), rgb)
case (true, false):
frame.setPixel((tileX + 7 - x, tileY + y), rgb)
case (false, true):
frame.setPixel((tileX + x, tileY + 7 - y), rgb)
case (true, true):
frame.setPixel((tileX + 7 - x, tileY + 7 - y), rgb)
}
}
}
}
}
static func renderNameTable(_ ppu: NesPPU, frame: Frame, nameTable: [UInt8], viewPort: Rect, shift: (x: Int, y: Int)) {
let bank = ppu.ctrl.backgroundPatternAddr()
let attributeTable = nameTable[0x3c0..<0x400]
for i in 0..<0x3c0 {
let tileAddr = UInt16(nameTable[i])
let tileLoc = (col: i % 32, row: i / 32)
let tile = ppu.chrRom[(bank + Int(tileAddr) * 16)...(bank + Int(tileAddr) * 16 + 15)]
let bgPalette = getBgPalette(ppu, tileLoc: tileLoc, attributeTable: Array(attributeTable))
// MARK: Draw Background
for y in 0...7 {
var upper = tile[tile.startIndex + y]
var lower = tile[tile.startIndex + y + 8]
for x in [7,6,5,4,3,2,1,0] {
let value = (1 & lower) << 1 | (1 & upper)
upper = upper >> 1
lower = lower >> 1
let rgb = switch value {
case 0:
NESColor.SYSTEM_PALLETE[Int(ppu.paletteTable[0])]
case 1:
NESColor.SYSTEM_PALLETE[Int(bgPalette[1])]
case 2:
NESColor.SYSTEM_PALLETE[Int(bgPalette[2])]
case 3:
NESColor.SYSTEM_PALLETE[Int(bgPalette[3])]
default:
fatalError("Invalid Pallete Color type")
}
let pixelLoc = (x: tileLoc.col * 8 + x, y: tileLoc.row * 8 + y)
if pixelLoc.x >= viewPort.x1 && pixelLoc.x < viewPort.x2 && pixelLoc.y >= viewPort.y1 && pixelLoc.y < viewPort.y2 {
frame.setPixel((shift.x + pixelLoc.x, shift.y + pixelLoc.y), rgb)
}
}
}
}
}
static func getBgPalette(_ ppu: NesPPU, tileLoc: (col: Int, row: Int), attributeTable: [UInt8]) -> [UInt8] {
let attrTableIndex = tileLoc.row / 4 * 8 + tileLoc.col / 4
let attrByte = attributeTable[attrTableIndex]
let palleteIndex = switch (tileLoc.col % 4 / 2, tileLoc.row % 4 / 2) {
case (0,0):
attrByte & 0b11
case (1,0):
(attrByte >> 2) & 0b11
case (0,1):
(attrByte >> 4) & 0b11
case (1,1):
(attrByte >> 6) & 0b11
default:
fatalError("Invalid titleLoc. This should never happen!")
}
let palleteStartIndex = 1 + Int(palleteIndex) * 4
return [
ppu.paletteTable[0],
ppu.paletteTable[palleteStartIndex],
ppu.paletteTable[palleteStartIndex + 1],
ppu.paletteTable[palleteStartIndex + 2],
]
}
static func getSpritePalette(_ ppu: NesPPU, paletteIndex: UInt8) -> [UInt8] {
let start = 0x11 + Int(paletteIndex * 4)
return [
0,
ppu.paletteTable[start],
ppu.paletteTable[start + 1],
ppu.paletteTable[start + 2]
]
}
}
struct Rect {
let x1: Int
let y1: Int
let x2: Int
let y2: Int
}

View File

@@ -21,93 +21,40 @@ SDL_RenderSetScale(canvas, 3.0, 3.0)
var texture = SDL_CreateTexture(canvas, SDL_PIXELFORMAT_RGB24.rawValue, Int32(SDL_TEXTUREACCESS_TARGET.rawValue), 256, 240)
var event = SDL_Event()
var quit = false
// func handleUserInput(_ cpu: CPU, event: inout SDL_Event) {
// while SDL_PollEvent(&event) > 0 {
// if event.type == SDL_QUIT.rawValue {
// SDL_DestroyWindow(window)
// SDL_Quit()
// exit(0)
// }
// if event.type == SDL_KEYDOWN.rawValue {
// switch SDL_KeyCode(UInt32(event.key.keysym.sym)) {
// case SDLK_ESCAPE:
// SDL_DestroyWindow(window)
// SDL_Quit()
// exit(0)
// case SDLK_w:
// cpu.memWrite(0xff, data: 0x77)
// case SDLK_a:
// cpu.memWrite(0xff, data: 0x61)
// case SDLK_s:
// cpu.memWrite(0xff, data: 0x73)
// case SDLK_d:
// cpu.memWrite(0xff, data: 0x64)
// default:
// continue
// }
// }
// }
// }
guard let bytes = NSData(contentsOfFile: "smb1.nes") else { fatalError("Rom not found") }
// func color(_ byte: UInt8) -> SDL_Color {
// switch byte{
// case 0:
// return SDL_Color(r: 0, g: 0, b: 0, a: 255)
// case 1:
// return SDL_Color(r: 255, g: 255, b: 255, a: 255)
// case 2, 9:
// return SDL_Color(r: 128, g: 128, b: 128, a: 255)
// case 3, 10:
// return SDL_Color(r: 255, g: 0, b: 0, a: 255)
// case 4, 11:
// return SDL_Color(r: 0, g: 255, b: 0, a: 255)
// case 5, 12:
// return SDL_Color(r: 0, g: 0, b: 255, a: 255)
// case 6, 13:
// return SDL_Color(r: 255, g: 0, b: 255, a: 255)
// case 7, 14:
// return SDL_Color(r: 255, g: 255, b: 0, a: 255)
// default:
// return SDL_Color(r: 0, g: 255, b: 255, a: 255)
// }
// }
// func readScreenState(_ cpu: CPU, frame: inout [UInt8]) -> Bool {
// var frame_idx = 0
// var update = false
// for i in 0x0200..<0x600 {
// let color_idx = cpu.memRead(UInt16(i))
// let color = color(color_idx)
// let (b1, b2, b3) = (color.r, color.b, color.g)
// if frame[frame_idx] != b1 || frame[frame_idx + 1] != b2 || frame[frame_idx + 2] != b3 {
// frame[frame_idx] = b1;
// frame[frame_idx + 1] = b2;
// frame[frame_idx + 2] = b3;
// update = true;
// }
// frame_idx += 3
// }
// return update
// }
guard let bytes = NSData(contentsOfFile: "pacman.nes") else { fatalError("Rom not found") }
var gameCode = [UInt8](repeating: 0, count: bytes.length)
bytes.getBytes(&gameCode, length: bytes.length)
let rom = try Rom(gameCode)
let joypad1 = Joypad()
//let tileFrame = TileViewer.showTile(chrRom: rom.character, bank: 1, tileNum: 0)
let tileFrame = TileViewer.showTileBank(chrRom: rom.character, bank: 1)
let keyMap = [
SDLK_DOWN : JoypadButton.DOWN,
SDLK_UP : JoypadButton.UP,
SDLK_LEFT : JoypadButton.LEFT,
SDLK_RIGHT : JoypadButton.RIGHT,
SDLK_SPACE : JoypadButton.SELECT,
SDLK_RETURN : JoypadButton.START,
SDLK_a : JoypadButton.BUTTON_A,
SDLK_s : JoypadButton.BUTTON_B
]
SDL_UpdateTexture(texture, nil, tileFrame.data, 256 * 3)
SDL_RenderCopy(canvas, texture, nil, nil)
SDL_RenderPresent(canvas)
var timer = Date.now.addingTimeInterval(0.01666)
var frame = Frame()
let bus = Bus(rom: rom, joypad1: joypad1) { ppu in
Render.render(ppu, frame: frame)
// wait here until 16.66 ms have passed
// i would use clock_nanosleep but that isn't available cross platform
if timer.timeIntervalSinceNow > 0 {
usleep(UInt32(timer.timeIntervalSinceNow * 1000000))
}
timer.addTimeInterval(0.01666)
SDL_UpdateTexture(texture, nil, frame.data, 256 * 3)
SDL_RenderCopy(canvas, texture, nil, nil)
SDL_RenderPresent(canvas)
while true {
while SDL_PollEvent(&event) > 0 {
if event.type == SDL_QUIT.rawValue {
SDL_DestroyWindow(window)
@@ -115,44 +62,24 @@ while true {
exit(0)
}
if event.type == SDL_KEYDOWN.rawValue {
switch SDL_KeyCode(UInt32(event.key.keysym.sym)) {
let keyCode = SDL_KeyCode(UInt32(event.key.keysym.sym))
switch keyCode {
case SDLK_ESCAPE:
SDL_DestroyWindow(window)
SDL_Quit()
exit(0)
default:
continue
guard let key = keyMap[keyCode] else { continue }
joypad1.setButton(key, pressed: true)
}
}
if event.type == SDL_KEYUP.rawValue {
guard let key = keyMap[SDL_KeyCode(UInt32(event.key.keysym.sym))] else { continue }
joypad1.setButton(key, pressed: false)
}
}
}
// let bus = Bus(try! Rom(gameCode))
//var cpu = CPU(bus: bus)
//cpu.load(gameCode)
//cpu.reset()
//cpu.programCounter = 0xC000
// var screenState = [UInt8](repeating: 0, count: 32 * 3 * 32)
// var rng = SystemRandomNumberGenerator()
// cpu.run(onCycle: {
// //print(dumpCpuState(cpu))
// handleUserInput(cpu, event: &event)
// cpu.memWrite(0xfe, data: UInt8.random(in: 1...16, using: &rng))
// if readScreenState(cpu, frame: &screenState) {
// SDL_UpdateTexture(texture, nil, screenState, 32 * 3)
// SDL_RenderCopy(canvas, texture, nil, nil)
// SDL_RenderPresent(canvas)
// }
// usleep(70)
// }, onComplete: {
// SDL_DestroyWindow(window)
// SDL_Quit()
// exit(0)
// })
// Infinite loop otherwise the program will exit prematurely
// RunLoop.main.run()
let cpu = CPU(bus: bus)
cpu.reset()
cpu.run()