Skip to content

Draw the sprites the chip drops, from a Hacks menu or --hack - #37

Merged
dimiro1 merged 1 commit into
masterfrom
dimiro1/hack-menu-sprite-limits
Aug 20, 2026
Merged

Draw the sprites the chip drops, from a Hacks menu or --hack#37
dimiro1 merged 1 commit into
masterfrom
dimiro1/hack-menu-sprite-limits

Conversation

@dimiro1

@dimiro1 dimiro1 commented Aug 20, 2026

Copy link
Copy Markdown
Owner

The 2C02 draws eight sprites a scanline, which is why so many games flicker theirs; a new Hacks menu, --hack unlimited-sprites and a hack NAME on|off REPL command draw the ones the chip would have dropped.

Nothing a game can observe changes — the evaluation, the eight real units and every bus cycle they make are untouched, the overflow flag still rises, and OAM and the patterns are read through peek, so MMC3's scanline counter never sees an address that would not have been there; it is one nested class in PPU, off unless asked for, with its output units in the save state but not its switch, and run.hacks in the report is read back off the machine so a flip part way through a session is reported truthfully.

It also fixes SaveStateCompletenessTests, which named array elements after their class and so would have left eight of the PPU's sprite output units silently uncompared once there were two SpriteUnit[] arrays to name.

No real cartridge overflows anywhere a test can reach — Punch-Out!!'s first fight peaks at seven sprites on a line and Battletoads' first level at eight — so sprite-limit.nes is vendored along with the Java that assembles it and the asm6 source it reads as, held together by a test; mvn -B clean test is green at 1630 tests, and the desktop menu is the one thing not exercised by hand.

🤖 Generated with Claude Code

The 2C02 has eight sprite output units, so a scanline that wants a ninth loses
one -- which is why so many games flicker their sprites, rotating which of them
is dropped so that all of them are visible half the time. A new **Hacks** menu,
and `--hack unlimited-sprites` beside it, draws the dropped ones as well.

Nothing a game can observe changes. The dot-accurate machinery is untouched:
evaluation, secondary OAM, the eight real units and every bus cycle they make
run exactly as they did, and this runs afterwards on the results. The overflow
flag still rises, $2004 still answers with whatever the sprite hardware is
holding, and sprite 0 hit is still sprite 0's. OAM is read through `OAM.peek`
and the patterns through `VRAM.peek`, so no row of OAM is refreshed that would
have decayed and MMC3's scanline counter never sees an address that would not
have been there -- `PPUAddressBusTests` holds the whole mapper-visible address
sequence over a nine-sprite line against the same line with the hack off.

It is one nested class in `PPU`. `ExtraSprites` owns fifty six `SpriteUnit`s,
scans OAM once a line at dot 320 for whatever the evaluation had no room for,
and the multiplexer asks it for a pixel only once all eight real units have come
out transparent -- every extra sprite is later in OAM than every real one, so
first-opaque-wins still means what it did. The walk starts where the evaluation
started rather than at sprite 0, so a game that left OAMADDR somewhere odd does
not get sprites resurrected from in front of where the hardware began looking.
The units travel in a save state even though the switch does not, because a
state can be taken half way down a scanline and resuming from one has to draw
the rest of that line the way running straight through would have.

Off everywhere unless it is asked for, and `run.hacks` in the report reads it
back off the machine rather than off the command line -- so a `hack
unlimited-sprites on` part way through an interactive session is reported as the
run ended rather than as it started. It joins `run.region` and
`run.state.startedFromPowerOn` in what to check before diffing two reports.

Which turned up something in the test that had to change for it.
`SaveStateCompletenessTests` named the elements of an array after their class,
so the PPU's two `SpriteUnit[]` arrays would both have produced
`SpriteUnit[0..7]`: the second walked would have overwritten the first and eight
real units would have gone silently uncompared. They are named after the field
now.

No cartridge demonstrates this. A game is written to stay under eight sprites a
line and they mostly manage it -- Punch-Out!!'s first fight peaks at seven and
Battletoads' first level at eight -- so `sprite-limit.nes` is vendored for it,
with the Java that assembles it and the asm6 source it reads as. A test holds
the cartridge against the generator, so the bytes cannot outlive the code that
explains them, and the build still needs no assembler.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dimiro1
dimiro1 merged commit e8d8c42 into master Aug 20, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant