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
impl Ppu
sourcepub fn tick<M: Mapper, B: PixelBuffer>(
&mut self,
mapper: &mut M,
buffer: &mut B
)
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
.
sourcepub fn tick_to_next_vblank<M: Mapper, B: PixelBuffer>(
&mut self,
mapper: &mut M,
buffer: &mut B
)
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.
sourcepub fn tick_to_next_sprite_0_hit<M: Mapper, B: PixelBuffer>(
&mut self,
mapper: &mut M,
buffer: &mut B
)
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.
sourcepub fn set_oam(&mut self, sprites: [Sprite; 64])
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.
sourcepub fn set_oam_bytes(&mut self, bytes: [u8; 256])
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.
sourcepub fn write_addr(&mut self, b: u8)
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.
sourcepub fn write_scroll(&mut self, b: u8)
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.
sourcepub fn write_ctrl(&mut self, b: u8)
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.
sourcepub fn write_mask(&mut self, b: u8)
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.
sourcepub fn read_status(&mut self) -> u8
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 onPpu::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.
sourcepub fn read_data<M: Mapper>(&mut self, mapper: &mut M) -> u8
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.
sourcepub fn write_data<M: Mapper>(&mut self, mapper: &mut M, value: u8)
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.
sourcepub fn write_data_iter<M: Mapper, I>(&mut self, mapper: &mut M, values: I)where
I: IntoIterator<Item = u8>,
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]);
sourcepub fn write_oam_addr(&mut self, addr: u8)
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.
sourcepub fn read_oam_data(&mut self) -> u8
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()
.
sourcepub fn write_oam_data(&mut self, value: u8)
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()
.