Skip to content

Give the game more time a frame, from a Hacks menu or --hack overclock - #41

Merged
dimiro1 merged 1 commit into
masterfrom
dimiro1/overclock-hack
Aug 24, 2026
Merged

Give the game more time a frame, from a Hacks menu or --hack overclock#41
dimiro1 merged 1 commit into
masterfrom
dimiro1/overclock-hack

Conversation

@dimiro1

@dimiro1 dimiro1 commented Aug 24, 2026

Copy link
Copy Markdown
Owner

A game that asks for more than 29780 cycles a frame overruns its main loop and drops one — Super Mario Bros. 3 and Gradius under sprite load — which no amount of accuracy will fix, so --hack overclock=131, hack overclock LINES [MORE] in the REPL and Hacks > Overclock in the window do what Mesen's "additional scanlines before NMI / after NMI" does: the PPU runs the post-render line or the last line of blanking again, as many times as asked, and the CPU gets ~113.67 more cycles a line on NTSC. The whole of it is four lines in PPU.advance, and the scanline numbers never change — a great deal of the chip keys on 240, 241 and the pre-render line by number, and a repeated 240 is indistinguishable from a longer idle one — while two clocks are deliberately not stretched: clock++ is skipped on a repeat so OAM decay stays in the television's time (without it, anything past 270 lines would wipe every sprite in the game once a frame), and NES.tick calls a new APU.idle instead of APU.tick, which keeps pitch, tempo and the samples-per-frame count the hardware's — idle still does cycles++, because that counter's parity is what CPUBus.isGetCycle reads and a sprite DMA would otherwise take 513 cycles where the hardware takes 514.

Unlike unlimited-sprites this one changes what the game does, so it rides inside a movie in a new OVCK chunk and is put back on replay exactly as the Game Genie codes are, both front ends refuse to change it while a recording is running, --play refuses --hack overclock while still combining with --hack unlimited-sprites, and the setting stays out of a save state (the Hacks menu's tick) while the count of repeats the beam is part way through travels in one. Reach for the before-NMI number: extra post-render lines break nothing a game observes, where extra vblank lines move the picture relative to the NMI and can break a cycle-counted mid-screen split — which is why the desktop's presets are percentages of the region's own frame and put all their lines before it.

There is no shipped game that lags on demand, so overclock.nes lags on purpose: a lap of its main loop takes 42500 cycles, or 1.43 NTSC frames, and over 300 frames it manages 149 laps plain, 149 at overclock=66 — a longer frame that is still not long enough buys nothing at all — and 298 at overclock=131, with run.apuCycles still equal to run.cpuCycles and audio.samples moving by 3 in 220138; on a real Super Mario Bros. at 900 frames the picture hash and the sample count are identical with the hack and without it, and only the cycle count and frameChanges differ. OverclockROM assembles the cartridge beside SpriteLimitROM with an asm6 twin and a PROVENANCE block, run.hacks.overclock is always present with both halves so two reports still compare key for key, and there are no new dependencies, no new fields on any chip outside the PPU, and mvn -B clean test and scripts/smoke-distribution.sh are green.

🤖 Generated with Claude Code

A NES game does a frame's worth of work between one NMI and the next, and when
that work does not fit the main loop overruns: the next NMI finds it unfinished,
a lag frame goes by, and the picture stutters. Super Mario Bros. 3 and Gradius
under sprite load are the cases everybody knows, and nothing an accurate emulator
does will fix them, because it is not a fault in the console -- it is a cartridge
asking for more cycles than 29780. What every mature emulator offers instead is
this, which Mesen calls "additional scanlines before NMI / after NMI": the PPU
idles through extra scanlines, the CPU gets ~113.67 more cycles a line on NTSC
and ~106.56 on PAL, and the game finishes in time.

The whole of it is four lines in `PPU.advance`. On the wrap of the post-render
line the beam runs line 240 again, and on the wrap of the last line of blanking
it runs that one again -- so the extra post-render lines land between the picture
and the VBlank flag going up, and the extra vblank lines between the end of
blanking and the pre-render line with the flag still up. **The scanline numbers
never change**, which is what makes this small: a great deal of the chip keys on
240, 241 and the pre-render line by number, and a repeated 240 is
indistinguishable from a longer idle one. The rejected alternative was Mesen's,
which shifts the line numbers instead and touches every comparison against them.

Two clocks are deliberately not stretched. `clock++` is skipped on a repeat,
because what that counter is measured against is OAM losing its charge and the
charge leaks in the television's time -- without the skip, anything past 270
lines on NTSC would push a frame's blanking past `Region.oamDecayDots` and every
sprite in the game would vanish once a frame. And `NES.tick` calls a new
`APU.idle` instead of `APU.tick` on those cycles, which is what keeps pitch,
tempo and the samples-per-frame count the hardware's; Mesen stops its APU on the
extra lines for the same reason. `idle` still does `cycles++`, and that is
mandatory rather than tidy: the parity of that counter is what `CPUBus.isGetCycle`
reads, the MMU asks the same question of the CPU's counter when it starts a DMA,
and the two only agree because both advance once per `NES.tick`. A counter that
stood still for 131 scanlines would come back inverted and a sprite DMA would
take 513 cycles where the hardware takes 514.

**Unlike unlimited-sprites this is a timing hack, so it changes what the game
does**, and everything else follows from that. It rides inside a movie in a new
`OVCK` chunk and is put back on replay, exactly as the Game Genie codes are and
for a sharper reason -- a take recorded with it and replayed without diverges
within a second. Both front ends refuse to change it while a recording is
running. The setting is not in a save state, being the Hacks menu's tick like
`ExtraSprites.enabled` and `MMU.genie`; the count of repeats the beam is part way
through *is*, so a state taken mid-line runs on the way the machine it came from
did, and one loaded into a machine with the hack off moves on at the next wrap.

Reach for the before-NMI number. Extra post-render lines break nothing a game
observes except that the frame is longer; extra vblank lines move the pre-render
line, and so the picture, relative to the NMI, which is what code counting cycles
down to a mid-screen split is measuring. Either way the pre-render line arrives
later in CPU cycles, so a program that waits out the warm-up by counting 29658
cycles rather than by waiting for two VBlanks has its first $2000/$2001 writes
dropped -- the same class of difference PAL's fifty extra lines make. The desktop
therefore offers percentages of the region's own frame and puts all of them
before the NMI; after-NMI is reachable from the command line and the REPL.

There is no game to demonstrate this on. A cartridge only lags where it is
loaded, which is not somewhere a test can reliably reach, so `overclock.nes` lags
on purpose and lags every time: a lap of its main loop takes 42500 cycles, which
is 1.43 NTSC frames, so it finishes one lap every two frames on the hardware and
one a frame at `overclock=131`. It counts frames at $00-$01 and laps at $02-$03
and recolours the whole screen once a lap through the background palette hack, so
`--dump ram` and `video.frameChanges` both answer. `OverclockROM` assembles it
beside `SpriteLimitROM`, with an asm6 twin and a `PROVENANCE` block.

Measured on it over 300 frames: 149 laps plain, 149 at `overclock=66` -- a longer
frame that is still not long enough buys nothing at all -- and 298 at
`overclock=131`. The cycle deltas are exact, `run.apuCycles` still equals
`run.cpuCycles`, and `audio.samples` moves by 3 in 220138. On Super Mario Bros.
at 900 frames the picture hash and the sample count are identical with the hack
and without it, and only the cycle count and `frameChanges` differ.

`run.hacks.overclock` is always present with both halves, so two reports still
compare key for key, and `ppu.onExtraLine` is there to explain a run that stopped
on line 240 and looks stuck. No new dependencies, no new fields on any chip
outside the PPU, and `--play` refuses `--hack overclock` while still combining
with `--hack unlimited-sprites`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dimiro1
dimiro1 merged commit 5b9172b into master Aug 24, 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