Struct nes_ppu::Ppu

source ·
pub struct Ppu { /* private fields */ }
Expand description

The NES Picture Processing Unit.

The Picture Processing Unit, or PPU, renders each frame of video line-by-line, pixel-by-pixel. It starts in the top left corner of the screen, moves to the right, and then continues from the leftmost pixel of the next row underneath the previous.

Scanlines

Each row of pixels is referred to as a scanline. After a full row of 256 pixels is rendered, the PPU has to wait a short amount of time (85 ticks) before it can begin outputting pixels for the next scanline. During this waiting period, the PPU fetches graphics data from memory that it will need for the next scanline. This waiting period at the end of each scanline is referred to as horizontal blanking or hblank.

Because the PPU is not rendering during hblank, and thus some parts of its operation are idle, hblank provides a short window for programs to interfere with the state of rendering in the middle of a frame, albeit in a limited capacity.

Frames

Each frame of video the NES outputs is 256x240 pixels. After the 240th scanline is complete, the PPU enters a new blanking period called vertical blanking or vblank, which lasts for about 20 scanlines’ worth of time (depending on how you count). During this period, the PPU is completely idle, meaning this is the span of time where programs should write data into VRAM, copy new sprite information into OAM, etc.

OAM

Object Attribute Memory, or OAM, is a 256 byte array stored internally within the PPU containing information about all the sprites currently on screen. Each sprite takes up 4 bytes, so OAM can alternately be thought of as an array of 64 sprites.

Each scanline, the PPU reads through OAM to see which sprites should be rendered on that line, which it determines by comparing their y-coordinates, the sprite size configured in the ctrl register, and the current scanline number. It picks the first 8 candidates it finds, and renders them on the next scanline.

This means that there are two important things to consider when placing sprite data into OAM:

  • Sprites are rendered one pixel lower than their specified y-coordinate.
  • Only 8 sprites are rendered per scanline, with ones coming later in OAM being ignored.

A common strategy on the NES to prevent sprites from “disappearing” when too many appear on a single row of pixels is to shuffle the order that sprites appear in OAM every frame. This way, instead of some sprites disappearing, all the sprites on that row “flicker,” because the sprites that get ignored are different every frame.

There is no way to disable a sprite; all 64 sprites are always active at once. However, you can hide a sprite by setting its y-coordinate below the visible area of the screen, i.e., to any value ≥ 240, most commonly 0xFF.

Implementations§

source§

impl Ppu

source

pub fn new() -> Self

Creates a new PPU instance in an unspecified state.

source

pub fn tick<M: Mapper, B: PixelBuffer>( &mut self, mapper: &mut M, buffer: &mut B )

Run the PPU for 1 cycle. This may induce memory accesses through the mapper, as well as outputting pixel information to the buffer.

source

pub fn tick_to_next_vblank<M: Mapper, B: PixelBuffer>( &mut self, mapper: &mut M, buffer: &mut B )

Run the PPU until it would emit an interrupt signalling that it has entered the vertical blanking period. If the PPU is already in vblank, this will tick until the vertical blank of the next frame.

Just like Ppu::tick(), this function may induce memory accesses through mapper and output pixel information to buffer.

Once this function returns, a full frame will have been rendered to buffer, which is a good time to output its contents to the screen.

source

pub fn tick_to_next_sprite_0_hit<M: Mapper, B: PixelBuffer>( &mut self, mapper: &mut M, buffer: &mut B )

Run the PPU until the sprite-0-hit flag is set in the status register. If the flag is already set, this will run until it is cleared and then set again.

Just like Ppu::tick(), this function may induce memory accesses through mapper and output pixel information to buffer.

Once this function returns, the PPU will have just output the first pixel where a non-transparent pixel of the sprite in slot 0 overlaps a non-transparent pixel of a tile. This is a good way to time mid-frame raster effects like split scrolling.

source

pub fn set_oam(&mut self, sprites: [Sprite; 64])

Overwrites the contents of OAM with the given sprite array.

This is a convenience utility meant to partially replicate the behavior of OAM DMA on the actual NES, but it is not emulated precisely.

source

pub fn set_oam_bytes(&mut self, bytes: [u8; 256])

Overwrites the contents of OAM with the given byte array.

This is a convenience utility meant to partially replicate the behavior of OAM DMA on the actual NES, but it is not emulated precisely.

source

pub fn write_addr(&mut self, b: u8)

Sets the high or low byte of the address for VRAM data accesses.

Calling write_addr() the first time will set the high 6 bits of the VRAM address (the high 2 bits of the input are ignored), and calling it again will set the low 8 bits. Thus, write_addr() is usually called twice in succession.

Writing to either half of the address writes to the internal T register. When writing to the low 8 bits, the internal V register is set to the new value of T afterwards. This means that the effective address that VRAM data accesses will use is only updated after setting the low bits.

let mut ppu = Ppu::new();
ppu.write_addr(0x20);       // sets the high 6 bits of the address
ppu.write_addr(0x01);       // sets the low 8 bits of the address & updates effective address
// the address now contained in T and V is 0x2001.

Whether or not write_addr() updates the high or low bits of the address is dependent on the internal W latch, which is also modified by Ppu::write_scroll() and Ppu::read_status(). Here is an example of interfering with W in the middle of writing an address:

let mut ppu = Ppu::new();   // W latch = 0
ppu.write_addr(0x20);       // W = 0, so sets the high bits of the address (now W = 1)
_ = ppu.read_status();      // clears W, now W = 0
ppu.write_addr(0x24);       // W = 0, so sets the high bits again (now W = 1)
ppu.write_addr(0x00);       // W = 1, so sets the low bits and sets V to T (now W = 0)

Read more about the addr register on the NESdev Wiki.

Read more about how setting the address interacts with W, T, and V on the NESdev wiki.

source

pub fn write_scroll(&mut self, b: u8)

Sets the value of x or y scrolling relative to the current nametable selected in the ctrl register.

Calling write_scroll() the first time will set the x scroll, and calling it again will set the y scroll. Thus, write_scroll() is usually called twice in succession:

let mut ppu = Ppu::new();
ppu.write_scroll(100);      // sets the x scroll
ppu.write_scroll(50);       // sets the y scroll

Note that x and y scroll values modify the value of the internal T register. Also, nametables are only 240 pixels tall, so setting the y scroll to a number ≥240 will cause garbage tiles to be displayed.

Whether or not this function updates the x or y scroll is dependent on the internal W latch. Both Ppu::write_addr() and Ppu::read_status() also affect this latch.

Here is an example where W is interfered with between the two writes:

let mut ppu = Ppu::new();   // W latch = 0
ppu.write_scroll(100);      // because W = 0, sets the x scroll (now W = 1)
_ = ppu.read_status();      // clears W, now W = 0
ppu.write_scroll(100);      // because W = 0, sets the x scroll again (now W = 1)
ppu.write_scroll(50);       // because W = 1, sets the y scroll (now W = 0 again)

Read more about the scroll register on the NESdev Wiki.

Read about how scroll interacts with W and T on the NESdev Wiki.

source

pub fn write_ctrl(&mut self, b: u8)

Sets the value of the ctrl register.

This updates the current nametable, the sprite pattern table, the tile pattern table, sprite sizes, and the automatic address increment. The nametable select bits are also copied into the internal T register.

Note that, to emulate the short-circuiting behavior of the NES when bit 6 of ctrl is set, calling write_ctrl() with a value that has bit 6 set will induce a panic.

Read more about the ctrl register on the NESdev Wiki.

source

pub fn write_mask(&mut self, b: u8)

Sets the value of the mask register.

This controls greyscale, sprite rendering, tile rendering, sprite rendering in the leftmost 8 pixels, tile rendering in the leftmost 8 pixels, and color emphasis.

Read more about the mask register on the NESdev Wiki.

source

pub fn read_status(&mut self) -> u8

Get info about the current PPU status for the frame.

Returns a bitset containing 3 flags. The remaining 5 bits have unspecified values. The flags are as follows:

  • Bit 5: Supposed to indicate that this frame, the PPU has evaluated a scanline where more than 8 sprites would have to be drawn, resulting in dropout. This flag is bugged and does not work intuitively, which this library emulates.
  • Bit 6: Indicates that this frame, a non-transparent pixel of the sprite in OAM index 0 has overlapped with a non-transparent pixel of a tile.
  • Bit 7: Indicates whether the PPU currently in vblank. This flag is cleared after calling read_status(). Additionally, due to race conditions, this flag is bugged on actual NES hardware. For authenticity, try to rely on Ppu::tick_to_next_vblank() instead.

Calling read_status() also clears the W latch, which affects future calls to Ppu::write_addr() and Ppu::write_scroll().

Read more about the status register on the NESdev Wiki.

Read more about the bugged sprite overflow flag on the NESdev Wiki.

Read more about the W latch on the NESdev Wiki.

source

pub fn read_data<M: Mapper>(&mut self, mapper: &mut M) -> u8

Reads the value at the currently stored address.

This function reads the value at the location specified by the current address stored in the PPU, but does not necessarily return that value. The address is usually set up by calling Ppu::write_addr(). If the current address is in the range 0x0000..=0x3EFF, then this function will return the value stored in an internal read buffer, then perform the memory fetch through the Mapper::read() method of mapper, and update the internal read buffer with the result.

After the read is performed, the current address will automatically increment by either 1 or 32, depending on the value in the ctrl register.

ppu.write_ctrl(0); // set automatic increment to 1
// set address to 0x2000
ppu.write_addr(0x20);
ppu.write_addr(0x00);

// Discard whatever is currently in the read buffer.
// Then update read buffer with value in 0x2000 and increment address to 0x2001.
_ = ppu.read_data(&mut mapper);

// Set n to the value fetched from 0x2000 in the previous read.
// Then update read buffer with value in 0x2001 and increment address to 0x2002
let n = ppu.read_data(&mut mapper);

// Set m to the value fetched from 0x2001 in the previous read.
// Then update read buffer with value in 0x2002 and increment address to 0x2003
let m = ppu.read_data(&mut mapper);

If the address is in the range 0x3F00..=0x3FFF, then this function will directly return the value stored within internal palette memory at that address. However, the internal read buffer is still updated with the value obtained from Mapper::read() on mapper.

ppu.write_ctrl(0); // set automatic increment to 1
// set address to 0x3F00
ppu.write_addr(0x3F);
ppu.write_addr(0x00);

// Set n to value in palette memory at 0x3F00.
// Update read buffer to mapper.read(0x3F00), and increment address to 0x3F01.
let n = ppu.read_data(&mut mapper);

The address that is actually accessed is the low 14 bits of the internal V register. The 15th bit is completely ignored by read_data(), and the automatic address increment will not carry into the 15th bit.

Read more about the data register on the NESdev Wiki.

Read more about the internal V register on the NESdev Wiki.

source

pub fn write_data<M: Mapper>(&mut self, mapper: &mut M, value: u8)

Writes to the value at the currently stored address.

This function will write value to the location specified by the current address stored in the PPU. The address is usually set up using Ppu::write_addr(). If the current address is in the range 0x0000..=0x3EFF, the write will invoke the Mapper::write() method of mapper. Otherwise, if the address is in the range 0x3F00..=0x3FFF, the mapper will not be accessed and the write will instead be directed into internal palette memory.

After the write is performed, the current address will automatically increment by either 1 or 32, depending on the value in the ctrl register.

ppu.write_ctrl(0); // set automatic increment to 1
// set address to 0x2000
ppu.write_addr(0x20);
ppu.write_addr(0x00);

ppu.write_data(&mut mapper, 10); // write 10 at address 0x2000
ppu.write_data(&mut mapper, 15); // write 15 at address 0x2001

The address that is actually accessed is the low 14 bits of the internal V register. The 15th bit is completely ignored by write_data(), and the automatic address increment will not carry into the 15th bit.

Read more about the data register on the NESdev Wiki.

Read more about the internal V register on the NESdev Wiki.

source

pub fn write_data_iter<M: Mapper, I>(&mut self, mapper: &mut M, values: I)where I: IntoIterator<Item = u8>,

Writes a sequence of bytes starting at the current address.

Repeatedly calls Ppu::write_data() for each element of values. Because write_data() automatically increments the current address, this will write each value into sequential memory locations. Note that depending on the value in the ctrl register, the values may be written consecutively or spaced apart by 32 bytes each.

This is a convenience utility not actually present when programming for the NES.

ppu.write_ctrl(0b100); // set automatic increment to 32
// set address to 0x2000
ppu.write_addr(0x20);
ppu.write_addr(0x00);

// Write to addresses 0x2000, 0x2020, 0x2040, 0x2060, and 0x2080.
ppu.write_data_iter(&mut mapper, [1, 2, 3, 4, 5]);
source

pub fn write_oam_addr(&mut self, addr: u8)

Sets the current oam address.

This function sets the PPU’s current 8-bit address into OAM. Since this address is used internally during rendering to evaluate which sprites are visible, writing to the OAM address in the middle of rendering can corrupt which sprites are drawn. The current OAM address is also automatically reset to 0 towards the end of vblank.

Due to internal hardware issues, writing to the OAM address can corrupt the contents of OAM itself, which this library emulates. As a result, this function is useless in a majority of cases.

This function is most commonly used in conjunction with Ppu::write_oam_data() or Ppu::read_oam_data() to access the contents of OAM. However, due to the aforementioned issue where calling this function corrupts OAM, this is rarely practical.

ppu.write_oam_addr(0x80);
ppu.write_oam_data(0xFF); // write value 0xFF to address 0x80
let n = ppu.read_oam_data(); // read value at address 0x81

In almost all cases, to write to OAM, it is better to use Ppu::set_oam() or Ppu::set_oam_bytes() instead.

Read about the OAM addr register on the NESdev Wiki.

Read about the OAM memory layout on the NESdev Wiki.

source

pub fn read_oam_data(&mut self) -> u8

Reads the value pointed to by the current OAM address.

If the PPU is in vblank or rendering is disabled, then this function returns the value referred to by the currently stored OAM address. Otherwise, the PPU is in a busy state and this function will return the value most recently loaded by the PPU during its internal sprite processing routine. Unlike Ppu::write_oam_data(), this function does not automatically increment the OAM address.

// assume ppu is in vblank

// set current address to 0x55 (possibly causing OAM corruption)
ppu.write_oam_addr(0x55);
// read the value in OAM at address 0x55
let n = ppu.read_oam_data();
// assume ppu is currently rendering

// snoop the value the PPU most recently read from OAM during internal sprite processing
let n = ppu.read_oam_data();

See also: Ppu::write_oam_addr(), Ppu::write_oam_data().

Read more about the OAM data register on the NESdev Wiki.

source

pub fn write_oam_data(&mut self, value: u8)

Writes data into OAM at the current address.

If the PPU is in vblank or rendering is disabled, this function writes value into OAM at the current OAM address, then increments the current address by 1. This increment may cause an overflow from 0xFF back to address 0x00. If the PPU is currently rendering, then no write is performed, and the current OAM address is instead incremented by 4.

// assume ppu is in vblank

// set current address to 0x55 (possibly causing OAM corruption)
ppu.write_oam_addr(0x55);
// set the value in OAM at address 0x55 to 0xAA
ppu.write_oam_data(0xAA);
// set the value in OAM at address 0x56 to 0xBB
ppu.write_oam_data(0xBB);
// assume ppu is currently rendering

// perform no write and increment OAM address by 4 (this will interfere with sprite processing)
ppu.write_oam_data(0xAA);

See also: Ppu::write_oam_addr(), Ppu::read_oam_data().

Read more about the OAM data register on the NESdev Wiki.

Trait Implementations§

source§

impl Debug for Ppu

source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

§

impl RefUnwindSafe for Ppu

§

impl Send for Ppu

§

impl Sync for Ppu

§

impl Unpin for Ppu

§

impl UnwindSafe for Ppu

Blanket Implementations§

source§

impl<T> Any for Twhere T: 'static + ?Sized,

source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
source§

impl<T> Borrow<T> for Twhere T: ?Sized,

source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
source§

impl<T> BorrowMut<T> for Twhere T: ?Sized,

source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
source§

impl<T> From<T> for T

source§

fn from(t: T) -> T

Returns the argument unchanged.

source§

impl<T, U> Into<U> for Twhere U: From<T>,

source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

source§

impl<T, U> TryFrom<U> for Twhere U: Into<T>,

§

type Error = Infallible

The type returned in the event of a conversion error.
source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
source§

impl<T, U> TryInto<U> for Twhere U: TryFrom<T>,

§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.