PPU-3

今回は PPU のメモリやレジスタなど基本的な部分についてのコードを見ていきます。

毎度のことながらPPU の仕様や仕組みについては参考にしたページが詳しいので、そちらを参照してください。

PPU のメモリ

PPU にはいくつかのメモリ領域が搭載されています。

それぞれの実装についてコードを見ていきます。

キャラクターRAM

キャラクターRAM はスプライト情報を記憶しておくメモリで、0x0000-0x1FFF までの領域です。
これはサイズが 0x1000の領域 2つに分かれていて、それぞれを background, sprite 領域としています。
どちらが 0x0000 から始まって、どちらが 0x1000 から始まるのかは、PPUCTRL レジスタのビット3,4の値で決まります。

    var spriteTableOffset: Word {
        get {
            return registers[0][3] ? 0x1000 : 0x0000
        }
    }

    var backgroundTableOffset: Word {
        get {
            return registers[0][4] ? 0x1000 : 0x0000
        }
    }

キャラクターRAM は PPU からしかアクセスできません。
エミュレータ内では繋げる BUS は PPU_BUS になります。

以下はキャラクターRAM を PPU に接続する部分の Nes.Swift での実装です。
カートリッジ ROM がロードされると、最初にキャラクタ RAM 用にメモリを作成してカートリッジのキャラクター情報をコピーします。
次に、それを PPU_BUS に接続して PPU に接続しています。

        //
        // Create 16K RAM and copy character data from Cartridge
        //
        let characterMem = RAM(memory: [UInt8](repeating: 0x00, count: 0x4000)) // Create 16K RAM
        for i: Address in 0..<cartridge.characterROM.size() {
            let data: Byte = cartridge.characterROM.read(i)
            characterMem.write(i, data)
        }
        
        //
        //  PPU_BUS, Interrupts
        //
        let ppuBus = PPU_BUS(characterRAM: characterMem)
        let interrupts = Interrupts()

        self.ppu = PPU(bus: ppuBus, interrupts: interrupts, config: PPU.PPUConfig(isHorizontalMirror: false))

PPU からのアクセスは PPU_BUS を介して行われるます。
PPU+IO.swift では単純に以下のようにアクセスします。

    //
    //  Character RAM
    //
    func readCharacterRAM(_ addr: Word) -> Byte {
        return bus.read(addr)
    }
    
    func writeCharacterRAM(_ addr: Word, _ data: Byte) {
        bus.write(addr, data)
    }

PPU_BUS.swift でもそのままメモリにアクセスするだけです。

    func read(_ address: Address) -> Byte {
        return characterRAM.read(address)
    }
    
    func read(_ address: Address) -> Word {
        return characterRAM.read(address)
    }
    
    func write(_ address: Address, _ data: Byte) {
        characterRAM.write(address, data)
    }
    
    func write(_ address: Address, _ data: Word) {
        characterRAM.write(address, data)
    }
ビデオRAM(Background Graphics)

バックグラウンド画面に何を表示するかのデータを持っている name tables と attribute tables の領域です。
CPU からは PPU の PPUADDR, PPUDATA レジスタを介してアクセスします。
ビデオRAM は PPU 内ではプロパティとして持っています。

    var vRAM = RAM(memory: [Byte](repeating: 0x00, count: 0x2000))

アクセスは ReadVRam, WriteVRam が PPUADR で指定されたアドレスへの読み出し/書き込みを行なっています。
アドレスの指定が上位、下位バイトに分けて書き込みしなければならないのと、ミラーの計算などがあるのでそれらの処理も必要になっています。

    //
    //  VRAM
    //
    func readVRam() -> Byte {
        let buf = vRAMReadBuf
        if vRAMAddr >= 0x2000 {
            let addr = calcVRAMAddr()
            vRAMAddr += Word(vRAMOffset)
            if addr >= 0x3F00 {
                return vRAM.read(addr)
            }
            vRAMReadBuf = vRAM.read(addr)
        }
        else {
            vRAMReadBuf = readCharacterRAM(vRAMAddr)
            vRAMAddr += Word(vRAMOffset)
        }
        return buf
    }

    func writeVRAMAddr(_ data: Byte) {
        if isLowerVRAMAddr {
            vRAMAddr += Word(data)
            isLowerVRAMAddr = false
            isValidVRAMAddr = true
        }
        else {
            vRAMAddr = Word(data) << 8
            isLowerVRAMAddr = true
            isValidVRAMAddr = false
        }
    }
    
    func calcVRAMAddr() -> Word {
        if vRAMAddr >= 0x3000 && vRAMAddr < 0x3F00 {
            vRAMAddr -= 0x3000
            return vRAMAddr
        }
        else {
            return vRAMAddr - 0x2000
        }
    }

    func writeVRAMData(_ data: Byte) {
        if vRAMAddr >= 0x2000 {
            if vRAMAddr >= 0x3F00 && vRAMAddr < 0x4000 {
                palette.write(vRAMAddr - 0x3F00, data)
            }
            else {
                writeVRAM(calcVRAMAddr(), data)
            }
        }
        else {
            writeCharacterRAM(vRAMAddr, data)
        }
        vRAMAddr += Word(vRAMOffset)
    }
    
    func writeVRAM(_ addr: Word, _ data: Byte) {
        vRAM.write(addr, data)
    }
OAM

Object Attribute Memory です。画面に表示する Sprite 情報を保存しておく 256Byte のメモリです。
CPU からは PPU の OAMADDR, AOMDATA レジスタを介してアクセスします。また、OAM DMA を使ってメモリからデータを転送することも可能です。
PPU 内ではプロパティとして持っています。

    var spriteRAM = RAM(memory: [Byte](repeating: 0xFF, count: 0x0100))

アクセスは単純に決められたアドレスに読み書きするだけです。256Byte しかないのでアドレスの指定も簡単です。

    //
    //  Sprite RAM  (OAM)
    //
    func writeSpriteRAMAddr(_ data: Byte) {
        spriteRAMAddr = data
    }
    
    func writeSpriteRAMData(_ data: Byte) {
        spriteRAM.write(Word(spriteRAMAddr), data)
        spriteRAMAddr = spriteRAMAddr &+ 1
    }

OAM DMA は PPU の OAMDMA レジスタを介して行われます。
詳細は前回の記事で。

Secondary OAM

1 scan line に表示される Sprite 情報を持つバッファです。
今回のエミュレータでは 1画面分一気に Sprite を描画するので実装されていません。

PPU のレジスタ

PPU には以下の 8つのレジスタが CPU のメモリ領域にマップされています。
・0x2000 PPUCTRL
・0x2001 PPUMASK
・0x2002 PPUSTATUS
・0x2003 OAMADDR
・0x2004 OAMDATA
・0x2005 PPUSCROLL
・0x2006 PPUADDR
・0x2007 PPUDATA
それぞれの詳細については情報がたくさんあるので割愛します。
コード上も難しいことはしていないので、簡単に定義の部分だけ。
PPU のプロパティとして 8Byte 領域を確保しています。

そして、それぞれのレジスタの各ビットが持つ情報をプロパティとして取得できるようにしています。

    var registers = [Byte](repeating: 0, count: 0x08)

    var vRAMOffset: Byte {
        get {
            return (registers[0x00][2] ? 32 : 1)
        }
    }
    
    var nameTableId: Byte {
        get {
            return registers[0x00] & 0x03
        }
    }

    var vRAMOffset: Byte {
        get {
            return (registers[0x00][2] ? 32 : 1)
        }
    }
    
    var nameTableId: Byte {
        get {
            return registers[0x00] & 0x03
        }
    }
    
    var isSpriteBig: Bool {
        get {
            return registers[0x00][5]
        }
        set {
            registers[0x00][5] = newValue
        }
    }
    
    
    var hasVBlankIRQEnabled: Bool {
        get {
            return registers[0][7]
        }
    }
    
    var isBackgroundEnable: Bool {
        get {
            return registers[0x01][3]
        }
    }
    
    var isSpriteEnable: Bool {
        get {
            return registers[0x01][4]
        }
    }

    var backgroundTableOffset: Word {
        get {
            return registers[0][4] ? 0x1000 : 0x0000
        }
    }
    
    var spriteTableOffset: Word {
        get {
            return registers[0][3] ? 0x1000 : 0x0000
        }
    }

isSpriteBig はオリジナルにはありませんが、大きいスプライトに対応するならこんなプロパティが必要になるかな?と思って足してみました。

最後に

ここまで来てなんだけど、JavaScript の Nes Emulator 作った人がすっごいスライド作ってたのを発見した!
ファミコンエミュレータの創り方
ものすごい量でわかりやすい!
(今度は Rust だけど)今までここで書きたかったことがもっとわかりやすく書いてあるよ!
自分でも勘違いしてる箇所とかあるな〜

というわけで、もう上記リンクを見れば OK な気もしますが、折角なので KeyPad まではこのブログを更新します。
次回は Mac のキーボード入力を Nes に入れて nestest.nes を UI で動かします!