Reading and writing PPU memory

From SNESdev Wiki
Revision as of 01:29, 12 January 2023 by Undisbeliever (talk | contribs) (Add bullet point list about reading and writing OAM)
Jump to navigationJump to search

CGRAM

The PPU contains an internal 256 x 15bit memory called CGRAM that holds the palette data.


The S-CPU can access the CGRAM using the CGADD, CGDATA and CGDATAREAD registers.

  • The S-CPU can only access the CGRAM during Vertical Blank, Horizontal Blank or Force Blank.
    • If the CGDATA or CGDATAREAD registers are accessed during active-display the data will be read from or written to the wrong CGRAM address.
  • CGDATA is a write-twice register. You must always write to CGDATA an even number of times.
    • The color data is only written to the CGRAM on the second CGDATA write.
  • CGDATAREAD is a read-twice register. You should always read from CGDATAREAD an even number of times.
  • You should always set the CGRAM word address with CGADD before reading or writing to CGRAM.
    • This will also reset an internal odd/even counter.
    • Mixing CGRAM reads and writes is not recommended.
  • Each CGRAM color is 15 bits in size.
    • When writing to CGRAM, bit 15 is ignored
    • When reading CGRAM, bit 15 will be PPU2 open bus and should be masked.


To write to CGRAM, first set the CGRAM word address (ie, palette color index) with an 8-bit write to CGADD. Then preform two 8-bit writes to CGDATA. After the second write to CGDATA the color data will be written to CGRAM and the internal CGRAM word address will be incremented by one. Subsequent colors can be written to CGRAM with two more 8-bit writes to CGDATA.

.a8
.i16
// DB access registers
// REQUIRES: h-blank, v-blank or force-blank

    // Set a single CGRAM color at `COLOR_INDEX` to `COLOR_VALUE`

    // Set CGRAM word address (color index)
    lda     #COLOR_INDEX
    sta     CGADD

    // Write low byte
    lda     #.lobyte(COLOR_VALUE)
    sta     CGDATA

    // Write high byte
    lda     #.hibyte(COLOR_VALUE)
    sta     CGDATA
Variables:
    zpFarPtr - a 3 byte pointer in zero-page.


// Write a block of colors to CGRAM.
//
// INPUT: A = starting color index
// INPUT: X = number of colors to write (MUST BE > 0)
// INPUT: zpFarPtr = palette data
// REQUIRES: Vertical-Blank or Force-Blank.
//           (There is not enough Horizontal-Blank time to run this code)
.a8
.i16
// DB access registers
.proc WriteCgramBlock
    // Set CGRAM word address (color index)
    sta     CGADD

    ldy     #0
    Loop:
        // Write low byte
        lda     [zpFarPtr],y
        sta     CGDATA
        iny

        // Write high byte
        lda     [zpFarPtr],y
        sta     CGDATA
        iny

        dex
        bne     Loop
    rts
.endproc


Writing to CGRAM using DMA or HDMA is preformed using the One register, write twice transfer pattern (DMAP pattern 2). (See HDMA examples#HDMA to CGRAM for a HDMA example.)

Variables:
    cgramBuffer : uint16[256] = a buffer of 256 colors in RAM


// Transfer a 256 color buffer (`cgramBuffer`) to CGRAM using DMA channel 0
//
// REQUIRES: Vertical-Blank or Force-Blank
// DB access registers
// Uses DMA channel 0
subroutine TransferBufferToCgram:
    // reset CGRAM address
    CGADD = 0


    // DMA parameters: one write-twice register, to PPU
    DMAP0 = 2

    // B-Bus address
    BBAD0 = .lobyte(CGDATA)

    // A-Bus address
    A1T0  = .loword(cgramBuffer)
    A1B0  = .bankbyte(cgramBuffer)

    // Transfer size (SHOULD BE EVEN)
    DAS0  = .sizeof(cgramBuffer)

    // Start DMA transfer on channel 0
    MDMAEN = 1 << 0


Reading from CGRAM is preformed with the CGDATAREAD register in a similar manner as CGRAM writes. Bit 15 of the color data is open-bus and should be masked to 0.

VARIABLES: zpTmpWord - a temporary uint16 variable in zero-page.

// INPUT: A = color index to read
// OUTPUT: zpTmpWord = color value
// REQUIRES: v-blank or force-blank
.a8
.i16
// DB access registers
.proc ReadCgramColor
    sta     CGADD

    // Read low-byte
    lda     CGDATAREAD
    sta     zpTmpWord

    // Read high-byte
    lda     CGDATAREAD
    // (The MSB is open-bus and should be masked)
    and     #0x7f
    sta     zpTmpWord + 1

    rts
.endproc


OAM: Object Attribute Memory

The PPU contains two internal RAM blocks (a 512 byte low-table and a 32 byte hi-table) that form the OAM.


The S-CPU can access the OAM using the OAMADD, OAMDATA and OAMDATAREAD PPU registers.

  • The S-CPU can only access the OAM during Vertical Blank or Force Blank.
  • Writing to OAMADD sets an internal 9 bit OAM word address.
    • The 8th bit of the internal OAM word address (bit 0 of OAMADDH) determines which OAM table accessed.
    • The internal OAM address is reset whenever OAMADDL or OAMADDH is written to.
    • You should always set both OAMADDL and OAMADDH (eg, with a 16 bit write to OAMADD) when setting the OAM word address.
    • You should always write to OAMADD before transferring data to the OAM.
  • OAMDATA is a write-twice register when writing to the OAM low-table.
    • When writing to the low-table, the data is only written to the OAM on the second OAMDATA write.
    • When writing to the hi-table, the data is written to the OAM on every OAMDATA write.
    • Despite this, you should always treat OAMDATA as a write-twice register.
  • The internal OAM addresses is reset to the last value written to OAMADD when VBlank starts and the screen is enabled (not in force-blank).[1]
  • OAMADD can also enable OAM priority rotation.
    • When using OAM priority rotation, the first-sprite is updated and may be incremented on every OAMDATA write or OAMDATAREAD read.[2]
    • If you are using OAM priority rotation, you will need to write to OAMADD any after a OAM transfer to reset the first-sprite.


Reading and writing to OAM is the same as writing to CGRAM, except the OAM address register (OAMADD) is 16 bits wide.


It is highly recommended that you create a 544 byte OAM buffer in Work-RAM and only transfer data to the OAM via this buffer during the Vertical Blanking Period. (See VBlank routine#OAM_buffer_example for an example of a DMA transfer from an OAM buffer to OAM.)

  1. higan source code, sfc/ppu/object.cpp PPU::Object::scanline(), by Near
  2. higan source code, sfc/ppu/io.cpp and sfc/ppu/object.cpp, by Near (search for setFirstSprite())