NES PPU Sprite Evaluation: Secondary OAM, Overflow Bug, and Fetch Timing
Follow NES sprite evaluation from primary OAM through eight secondary slots, the diagonal overflow bug, sprite fetches, and scanline-visible state.
The NES Picture Processing Unit does not render all 64 sprites directly from primary OAM as pixels pass across the screen. During each rendered scanline it evaluates OAM entries for the following line, copies the selected sprite data into a small secondary OAM area, and later fetches patterns for a fixed number of sprite slots. The intended limit is eight sprites on a scanline, but the ninth-sprite detection contains a well-known hardware bug that software can observe through the sprite-overflow flag.
For an emulator, sprite evaluation is a timed subsystem with internal address counters and temporary state. Replacing it with a per-scanline loop that chooses the first eight intersecting rectangles may draw ordinary games correctly while setting overflow perfectly, which is not what the console does. More demanding software reads status, changes OAM or rendering state mid-frame, or uses overflow as an additional raster-timing signal.
Primary OAM and secondary OAM have different jobs
Primary OAM stores 64 four-byte sprite entries: Y coordinate, tile index, attributes, and X coordinate. The PPU scans those entries to find sprites whose vertical range intersects the next output line. The entries chosen for display are copied into secondary OAM, which has room for eight complete sprites. This early selection is why a sprite’s OAM order can affect which objects remain visible when a line exceeds the limit.
The Y comparison and line convention require care. A sprite’s stored Y value is offset relative to the scanline on which its first visible pixel appears, so a naive y <= line < y + height formula can be off by one. The 8x8 versus 8x16 mode also changes the sprite height and pattern-table interpretation. Use the PPU’s line numbering and sprite-range rule from the hardware reference, and keep rendering on the pre-render scanline distinct from evaluation for the first visible line.
Secondary OAM is not merely a host list. Its initialization, byte writes, overflow state, sprite index, and byte index all participate in the evaluation algorithm. The hardware scans through primary OAM using internal counters, and the operation consumes specific PPU cycles while background fetch and CPU access continue under the PPU’s timing model. Preserve those counters for mid-scanline state and save-state restoration.
Evaluation and fetch happen in separate phases
On the common NTSC 2C02 timing model, secondary OAM is cleared during the early part of the scanline, sprite evaluation runs through the middle portion, and sprite-pattern fetches occupy cycles 257 through 320. Evaluation prepares sprite records for the next line; fetching then loads the pattern bytes and attributes into the sprite output units. Those are distinct stages. A selected sprite can be correctly copied but later display incorrectly because the pattern address, vertical flip, 8x16 tile selection, or fetch timing is wrong.
Sprite fetch also performs work for unused slots. The PPU has a fixed sequence for eight output units, even when fewer sprites are selected. Dummy fetches and internal address values can matter to bus traces, open-bus reads, or a cycle-sensitive test. Do not skip the remaining hardware slots merely because the renderer has no more visible objects.
The evaluation window is tied to rendering enable. Turning off both background and sprite rendering can stop the process, while enabling either layer can preserve evaluation and bus effects even if one layer is visually hidden. Mid-frame changes to the mask register can therefore alter internal OAM behavior as well as pixel output. Add tests for each combination of background and sprite enable bits at evaluation boundaries.
Why sprite overflow is intentionally inaccurate
The status bit was intended to indicate that more than eight in-range sprites were present. After eight sprites have been copied, the PPU continues scanning to decide whether a ninth candidate exists. Because of the hardware’s secondary-OAM write-disable behavior and an error in the index logic, later checks do not continue reading only Y coordinates. The sprite number and byte index advance in a diagonal pattern, so tile, attribute, or X bytes from later OAM entries can be compared as though they were Y values.
That algorithm creates both false positives and false negatives. A ninth real sprite may not set the bit, and data that is not a Y coordinate can accidentally satisfy the comparison. A “corrected” emulator that simply sets overflow whenever the ninth sprite overlaps is cleaner but incompatible. Keep the documented intended rule separate from the observed bug and label tests that depend on the latter.
The overflow flag is sticky for the frame under normal behavior and is cleared at the pre-render boundary. It is part of the PPU status register and is observed by CPU reads with that register’s other side effects. Reading status can affect vblank state and the write toggle, so a test should not add an extra status read just to inspect overflow without accounting for its effects.
The overflow bug is also timing-sensitive. The evaluation counters advance in PPU dots while the CPU runs at a related but slower clock. A CPU write to OAM address or rendering control near an evaluation boundary can change what is scanned or expose model-specific corruption. Avoid implementing these interactions as a single atomic “evaluate line” function if the target aims to reproduce mid-scanline behavior.
Pixel priority is downstream of evaluation
Selecting a sprite does not decide its final priority against the background. Sprite attributes include palette selection, horizontal and vertical flipping, and a priority control that places the sprite behind or in front of background pixels. The renderer combines fetched sprite output with background pattern output and transparency rules. Keep evaluation order, sprite output-unit order, and final pixel priority as separate concepts.
Sprite zero has its own hit behavior. Its identity comes from the first OAM entry, but hit detection occurs when its nontransparent pixel overlaps an eligible background pixel under the PPU’s clipping and mask rules. Overflow evaluation does not create sprite-zero hit. A test suite should separately assert sprite selection, pattern fetch, sprite-zero hit, overflow status, and final color resolution.
This separation helps with common symptoms. A missing ninth object may be the normal eight-sprite line limit; a wrong front/back overlap is a priority defect; a false overflow flag is the evaluation bug; a missing split-screen trigger may be sprite-zero-hit timing. One screenshot cannot distinguish those causes.
A compact evaluation trace record
The following Python type is a test-log shape rather than a PPU implementation. Recording both counters makes the diagonal overflow behavior reviewable and keeps a test from reducing the whole scan to a final sprite count.
from dataclasses import dataclass
@dataclass(frozen=True)
class SpriteEvalStep:
dot: int
oam_index: int
byte_index: int
secondary_oam_bytes: int
overflow: bool
def validate_step(step):
if not 0 <= step.dot <= 340:
raise ValueError("PPU dot must be in the scanline")
if not 0 <= step.oam_index < 64 or not 0 <= step.byte_index < 4:
raise ValueError("primary OAM counters are out of range")
if not 0 <= step.secondary_oam_bytes <= 32:
raise ValueError("secondary OAM holds at most 32 sprite bytes")
Extend fixtures with PPU revision, scanline, mask-register state, initial OAM bytes, and every $2003 or $2004 access near the tested interval. A trace without those inputs cannot distinguish a core bug from a test that started evaluation with an unexpected OAM address.
Validate with structured patterns
Use OAM fixtures with zero through ten in-range sprites, then move one candidate across the range boundary. Change only the tile byte, attribute byte, or X byte of later entries to demonstrate the overflow bug’s diagonal checks. Test both sprite heights and both pattern-table arrangements. Confirm that the first eight eligible records enter secondary OAM in priority order and that later evaluation affects overflow without replacing them.
Add timing tests that inspect the internal counters just before evaluation begins, after the eighth sprite is copied, during overflow search, and during pattern fetch. Test both background-only and sprite-only rendering because either can keep PPU fetch/evaluation active. Run a stable scanline under NTSC and PAL timing models without reusing a dot-to-CPU-cycle ratio blindly; the frame geometry and PPU variants differ.
For a reference test, compare against a known test ROM and a well-established emulator implementation, then record whether the expected result comes from a documented register contract, a test-ROM consensus, or one emulator’s model. Where behavior has only been measured on particular 2C02 revisions, state the revision. Accuracy notes are stronger when uncertainty is explicit than when every quirk is presented as universal silicon law.
Acceptance criteria
A faithful sprite path models OAM clearing, selection of up to eight sprites, the buggy overflow walk, subsequent pattern fetches, sprite output priority, and CPU-visible PPU status as separate timed stages. It preserves mid-scanline counters in save states and can explain why a sprite was selected, dropped, fetched, hidden, or reported by the overflow flag.
Related:
- NES PPU VBlank and NMI Timing: Model the Event, Not Just the Frame
- Game Boy STAT and LYC Interrupts: Building Reliable Scanline Effects
Sources: