Give the game more time a frame, from a Hacks menu or --hack overclock - #41
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 inPPU.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), andNES.tickcalls a newAPU.idleinstead ofAPU.tick, which keeps pitch, tempo and the samples-per-frame count the hardware's —idlestill doescycles++, because that counter's parity is whatCPUBus.isGetCyclereads 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
OVCKchunk 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,--playrefuses--hack overclockwhile 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.neslags 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 atoverclock=66— a longer frame that is still not long enough buys nothing at all — and 298 atoverclock=131, withrun.apuCyclesstill equal torun.cpuCyclesandaudio.samplesmoving 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 andframeChangesdiffer.OverclockROMassembles the cartridge besideSpriteLimitROMwith an asm6 twin and aPROVENANCEblock,run.hacks.overclockis 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, andmvn -B clean testandscripts/smoke-distribution.share green.🤖 Generated with Claude Code