"PPU sprite evaluation" in need of clarification?

This is an archive of a topic from NESdev BBS, taken in mid-October 2019 before a server upgrade.
View original topic
"PPU sprite evaluation" in need of clarification?
by on (#147180)
Hello, everyone. I am modifying byuu's emulator, higan, to make the NES portion more accurate and increase compatibility. My fork is called nSide (Like higan, it uses cartridge folders and cannot run .nes or .unf ROMs in it. Use the Import function with .nes ROMs to make playable cartridge folders).

Famicom PPU sprite evaluation is an elusive concept that I am currently trying to implement, but I am struggling to understand the wiki article "PPU sprite evaluation", and my attempt at implementing it results in most NES games having every sprite flicker (I don't mean when a game tries to display more than 8 sprites; enemies and blocks in Super Mario Bros. will flicker even when there is only 1 of each). More specifically, it is not clear to me exactly at which times the PPU switches between the 4 modes during cycles 64-256. I already understand that the PPU reads from primary OAM during odd cycles and writes to secondary OAM during even cycles, and that cycles 1-64 are for filling the secondary OAM with 0xFF.

  • In step 2.1 (start at n = 0, sprite n's Y coördinate at OAM[n][0]), there is an instruction for when the Y coördinate is in range of the current scanline, which is to copy the tile ID, attributes, and X, but what if one is not found? Go to step 2, or stay on step 1?
  • In step 2.2 (Increment n), 2a states to go to step 4 if n has overflowed to 0, 2b states to go to step 1 if less than 8 sprites are found, but in 2c, which disables writes to secondary OAM when 8 sprites are found, no step number is specified. Does it go to step 2.3 when that happens?

I think I may be misinterpreting the article, but it would be much appreciated if the article could be amended with more verbose clarification about how going from step to step works.

I would like to use Visual 2C02 to analyze the behavior myself, but Visual 2C02 is on a limited uplink, and it would be nice if it was available to download so that I don't have to stress the server.

The current source code is available upon request 4 posts below.

EDIT 1: changed terminology to clarify that I am talking about sub-steps 2.1-2.4 in step 2.

EDIT 2: Now that I did post the source code, a sentence above had to be changed to indicate where it can be found.
Re: "PPU sprite evaluation" in need of clarification?
by on (#147182)
Each 2.1 step should take take 2 dots if Y is not in range, or 8 dots if Y is in range. The pointer into secondary OAM is increased if and only if Y is in range.
Re: "PPU sprite evaluation" in need of clarification?
by on (#147183)
Thanks, but I think it was already obvious how many cycles each step takes and how the secondary OAM pointer is incremented.

This is about which step to jump to when a step is done. For step 2.1, I want to know whether that step repeats when a sprite is not found in Y range, or whether to go to step 2.2 (increment n). I'm leaning towards going to step 2.2, but I'm not sure.
Re: "PPU sprite evaluation" in need of clarification?
by on (#147268)
Sprite evaluation has been giving me a lot of headache. I wrote some code for it in my emulator, but so far it just makes the entire screen grey, haha...
When I failed to understand the article, I gave visual 2C02 a try. Clearly it's not as simple as visual 6502, so I still don't completely understand how sprite eval works.
Re: "PPU sprite evaluation" in need of clarification?
by on (#147344)
I suppose I should post my sprite rendering code. I'm at a loss for where it goes wrong. The raster_sprite() method is called exactly 256 times during rendering (dots 1 to 256), or 8 times per background tile.
Code:
void PPU::raster_sprite() {
  if(raster_enable() == false) return;

  if(hcounter() < 65) {
    raster.oam_counter = (hcounter() - 1) >> 3;
    if(hcounter() & 1) {
      raster.oam_byte = 0xff;
      return;
    }
    switch(((hcounter() - 1) >> 1) & 3) {
    case 0: raster.soam[raster.oam_counter].y    = raster.oam_byte;        break;
    case 1: raster.soam[raster.oam_counter].tile = raster.oam_byte;        break;
    case 2: raster.soam[raster.oam_counter].attr = raster.oam_byte & 0xe3; break;
    case 3: raster.soam[raster.oam_counter].x    = raster.oam_byte;        break;
    }
    return;
  }
  if(hcounter() == 65) {
    raster.oam_iterator = 0;
    raster.oam_counter = 0;
    raster.oam_state = 0;
  }
  unsigned n = raster.oam_iterator >> 2;
  unsigned m = raster.oam_iterator & 3;
  if(hcounter() & 1) {
    raster.oam_byte = oam[raster.oam_iterator];
    return;
  }
  signed ly = (vcounter() == (system.region() == System::Region::NTSC ? 261 : 311) ? -1 : vcounter());
  unsigned y = ly - raster.oam_byte;
  switch(raster.oam_state) {
  case 0: // find sprite Y and verify it is in range
    if(raster.oam_counter < 8 && raster.oam_iterator < 256)
      raster.soam[raster.oam_counter].y = raster.oam_byte;
    if(y < sprite_height()) { // sprite in Y range
      if(raster.oam_counter < 8) raster.soam[raster.oam_counter].id = n;
      raster.oam_iterator++;
      raster.oam_state = 1;
    } else {
      raster.oam_state = 2;
    }
    break;
  case 1: // if sprite found in Y range, copy tile, attr, and x
    if(raster.oam_counter < 8) {
      switch(m) {
      case 1: raster.soam[raster.oam_counter].tile = raster.oam_byte; break;
      case 2: raster.soam[raster.oam_counter].attr = raster.oam_byte; break;
      case 3: raster.soam[raster.oam_counter].x    = raster.oam_byte; break;
      }
    }
    raster.oam_iterator++;
    if((raster.oam_iterator & 3) == 0) {
      raster.oam_counter++;
      raster.oam_state = 0;
    }
    break;
  case 2: // increment OAM Iterator by 4
    raster.oam_iterator += 4;
    if(raster.oam_iterator > 256) raster.oam_state = 5;
    if(raster.oam_counter < 8) raster.oam_state = 0;
    if(raster.oam_counter == 8) raster.oam_state = 3;
    break;
  case 3: // detect sprite overflows (when 8 sprites have been copied)
    if(y < sprite_height()) { // sprite in Y range
      status.sprite_overflow = 1;
      status.oam_state = 4;
    } else {
      raster.oam_iterator += 5; // sprite overflow bug increments by 5 instead of 4
      raster.oam_state = raster.oam_iterator < 256 ? 3 : 5;
    }
    break;
  case 4: // read next 3 bytes after detecting a sprite overflow
    if((++raster.oam_iterator & 3) == 0) raster.oam_state = 3;
    break;
  case 5: // attempt and fail to copy OAM (repeats until HBlank)
    raster.oam_iterator += 4;
    break;
  }
}

  • hcounter(): PPU horizontal counter. Will be between 1 to 256 when raster_sprite() is called.
  • raster.oam_iterator: Byte index in primary OAM. n and m are calculated from this value before every read.
  • raster.oam_counter: Number of sprites that have been written to secondary OAM.
  • raster.oam_state: Step. 0, 1, 2, 3, 4, 5 represent Steps 2.1, 2.1a, 2.2, 2.3, 2.3a, and 2.4 respectively.
Also, I found out something more for how the bug affects Super Mario Bros.: If one pauses the game by pressing START, the flickering will stop, and sometimes an enemy will be completely invisible until one unpauses again. Note that game emulation is still occurring while using the game's pause function. Flickering tends to only happen when an enemy sprite is moving.

I even fixed a few typos, which makes a download I had previously prepared outdated. I will prepare a new download of the source code tonight when I get back from work. If you do download the outdated version, look in fc/ppu/ppu.cpp and replace the raster_sprite() method with the one above before doing anything else.