Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 43 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,34 @@ and they mostly manage it -- Punch-Out!!'s first fight peaks at seven, Battletoa
eight -- so the demonstration is `sprite-limit/sprite-limit.nes`, which puts all sixty four on one
line and is assembled by `SpriteLimitROM` beside it rather than vendored as bytes.

**The other hack is a timing hack, and that is a different kind of thing.** `--hack overclock=131`
makes the PPU idle through 131 extra scanlines a frame, which is about 113.67 CPU cycles each on
NTSC, so a game whose main loop overruns its frame stops dropping one -- the every-other-frame
stutter Super Mario Bros. 3 and Gradius get under load. The picture is the hardware's, dot for dot,
because the extra lines are lines the beam is already idle on; the sound is a hardware frame's worth
too, because `APU.idle()` holds the sound chip still through them, so `audio.samples` and the music's
pitch and tempo do not move. What does move is **what the game does**, so two runs that disagree
about `run.hacks.overclock` are two different games rather than two views of one -- which is why,
unlike unlimited-sprites, it rides inside a movie, is refused while one is recording, and is refused
alongside `--play`.

`--hack overclock=131+20` puts twenty of the lines after the NMI instead. **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 exactly what code that cycle-counts 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 PPU's 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. `hack overclock LINES [MORE]`
and `hack overclock off` do it inside an interactive session, and `Overclock.percentOf` is what turns
the desktop's percentages into lines.

Do not go looking for a game to see this one on either. A game only lags where it is loaded, which
is not somewhere a test can reliably reach, so the demonstration is `overclock/overclock.nes`: 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`, so `--dump ram` reads the answer, and it recolours the whole screen once a lap so
`video.frameChanges` says the same thing. `OverclockROM` assembles it, beside `SpriteLimitROM`.

Everything that differs is in `Region`, including the PPU's OAM decay window, which has to outlast
the machine's own blanking interval or every sprite in the game vanishes once a frame. Its tables
are `static` on purpose: `SaveStateCompletenessTests` vandalises every primitive array it can reach
Expand Down Expand Up @@ -216,14 +244,16 @@ composes out of the rewind claim above: a rewound machine is byte for byte the m
went forward, so there is nothing lost by truncating the log to match.

`--play` is the input, so it refuses `--record`, `--input`, `--input-file`, `--reset-at`, `--genie`,
`--load-state`, `--sram-in` and `--interactive` -- each of those would be a second answer to a
question the movie has already answered. It defaults `--frames` to the movie's own length; asking for
more runs past the end with nothing held down.
`--hack overclock`, `--load-state`, `--sram-in` and `--interactive` -- each of those would be a
second answer to a question the movie has already answered. `--hack unlimited-sprites` still combines
with it, being a change to the picture and to nothing the replay depends on. It defaults `--frames`
to the movie's own length; asking for more runs past the end with nothing held down.

`record`, `record start` and `record stop [PATH]` do the same inside an interactive session -- the
shape of `rewind` rather than of `hack`, since the interesting form is the one that takes a file.
Mutating `genie`/`ungenie`/`genie clear` are refused while recording, because a movie pins the codes
at the moment it starts and a file naming one set that was played against another cannot be replayed.
Mutating `genie`/`ungenie`/`genie clear` and `hack overclock` are refused while recording, because a
movie pins both at the moment it starts and a file naming one set of codes, or one number of extra
scanlines, that was played against another cannot be replayed.

**`run.record` and `run.replay` join the comparability checklist**, beside `run.state`, `run.region`,
`run.hacks` and `run.genie`. Both are always present with explicit nulls. A run that started at power
Expand All @@ -234,13 +264,15 @@ a `--sram-in`, a loaded state mid-session, or a rewind that went back past the s

The Game Genie codes ride inside the movie and are put back on replay. They have to: a cheated
cartridge is byte for byte an honest one, so `cart.sha256` cannot tell them apart and nothing else in
the file would.
the file would. **The overclock rides in an `OVCK` chunk beside them**, for the same reason and a
sharper one: it decides how much of its work the game gets through in a frame, so a replay at the
hardware's timing is a replay of a different game rather than of the same game seen differently.

The desktop has **Machine > Record Movie... / Play Movie...**. While either is running the pad is
latched once a frame on the emulation thread rather than reaching the controller the moment a key
moves, and Power Cycle, Region and the Game Genie item are greyed out -- the first two would build a
new machine and take the recorder with it. Rewinding during playback stops it and hands the game
back.
moves, and Power Cycle, Region, the Game Genie item and the Overclock submenu are greyed out -- the
first two would build a new machine and take the recorder with it. Rewinding during playback stops it
and hands the game back, and the menu's own overclock goes back on when it does.

### Running a romhack

Expand Down Expand Up @@ -357,7 +389,8 @@ Four Maven modules, and the arrows between them only point one way.

```
mynes-core/ depends on nothing
mynes/ the console: CPU, PPU, APU, BUS, MMU, VRAM, Cart, Region, controllers
mynes/ the console: CPU, PPU, APU, BUS, MMU, VRAM, Cart, Region, Overclock,
controllers
mynes/mappers/ mappers 0 to 4
mynes/state/ save states, battery .sav files, and .mnm session recordings
mynes/debug/ the disassembler and the breakpoints, shared by the window and the REPL
Expand Down
34 changes: 23 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,15 @@ chip would have dropped, so a scanline holding more than eight of them stops fli
cannot tell, since the overflow flag still rises and the cartridge still sees the same address bus.
It is off unless it is ticked, and the tick is remembered.

**Game Genie codes**, from **Hacks > Game Genie...**, which is the other thing in that menu and the
**Overclock** is the other one, and it is not that kind of hack: it gives the game extra idle
scanlines a frame — +25%, +50%, +100% or +200% — so that a main loop which overruns its frame stops
dropping one, which is what the every-other-frame stutter in Super Mario Bros. 3 and Gradius under
load actually is. The picture is drawn exactly as the hardware draws it, and the music keeps its
pitch and tempo, because the sound chip stands still through the extra lines. But the game does get
more done between one frame and the next, so this is not the game as it shipped. It is remembered
like the tick above, and it is greyed out while a movie is recording or playing.

**Game Genie codes**, from **Hacks > Game Genie...**, which is the last thing in that menu and the
one thing in it the console really did do. Six letters or eight, from the sixteen a code is spelled
with; eight-letter codes carry a byte the cartridge has to answer with before they fire, which is
what pins one to a single bank. Nothing is patched — the device sat between the cartridge and the
Expand Down Expand Up @@ -333,22 +341,26 @@ seconds to start up, the jar about a third of one.
each one held, and `cart.sha256` is the digest of the patched image rather than the file on disk —
a patch that turns out to hold no records is one cut against a different dump of the game.
- **`--hack NAME`** switches on one of the things the console does not do, all of which are off
otherwise. There is one so far, `unlimited-sprites`, which draws the sprites the chip would have
dropped so that a scanline holding more than eight of them stops flickering. Nothing a game can
observe changes — the overflow flag still rises and the cartridge sees the same address bus — but
the picture is not the one the hardware would have produced, so `run.hacks` in the report is part
of what to check before diffing two of them.
otherwise, and `run.hacks` in the report is part of what to check before diffing two runs. There
are two. `unlimited-sprites` draws the sprites the chip would have dropped so that a scanline
holding more than eight of them stops flickering; nothing a game can observe changes — the
overflow flag still rises and the cartridge sees the same address bus — but the picture is not the
one the hardware would have produced. `overclock=N[+M]` adds N idle scanlines a frame before the
NMI and M after it, which is about 113.67 CPU cycles each on NTSC, so a game whose main loop
overruns its frame stops dropping one. That one is a *timing* hack: the picture is the hardware's
and so is the sound, but the game gets more done between frames, which makes an overclocked run
and a plain one two different games rather than two views of one.
- **`--genie CODE`** puts a Game Genie code in the cartridge slot. Repeatable, and comma separated.
Unlike `--patch` the cartridge is not modified at all — the device answered the bus in its place —
so `cart.sha256` is the plain one and `run.genie` is the only thing in the report that tells a
cheated run from an honest one. Which also means a save state taken with codes in will load into a
machine with them out, without a word of complaint.
- **`--record FILE` and `--play FILE`** write and replay a session. `--play` is the input, so it
refuses `--input`, `--reset-at`, `--genie`, `--load-state` and the rest of what a movie already
says, and it defaults `--frames` to the movie's own length -- ask for more and the run carries on
past the end with nothing held down. `run.record` and `run.replay` in the report join the list of
things to check before diffing two runs. `record start` and `record stop` are commands in the
interactive session as well.
refuses `--input`, `--reset-at`, `--genie`, `--hack overclock`, `--load-state` and the rest of
what a movie already says, and it defaults `--frames` to the movie's own length -- ask for more
and the run carries on past the end with nothing held down. `run.record` and `run.replay` in the
report join the list of things to check before diffing two runs. `record start` and `record stop`
are commands in the interactive session as well.
- **`--interactive`** reads commands on standard input and answers each with a line of JSON, for
when you do not yet know the question well enough to write it down. It is also where the debugger
lives without a window: `break`, `watch`, `step` and `disasm`, with `run` reporting back what
Expand Down
49 changes: 47 additions & 2 deletions mynes-core/src/main/java/com/github/dimiro1/mynes/APU.java
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,49 @@ public void tick() {
sample();
}

/**
* Advances the chip's parity and nothing else, for a CPU cycle the {@link Overclock} hack has
* added to the frame.
*
* <h2>Why the sound stands still</h2>
*
* An overclocked frame is longer in CPU cycles but is still one frame of the game. If the APU
* ran through the extra lines, everything it counts in cycles would run with them: the frame
* counter's quarter and half frames -- so every envelope, sweep and length counter -- the five
* timers, and the decimator that turns 1.79MHz into 44100 samples a second. The music would come
* out slow, the pitches flat and the frame a thousand samples instead of 735, which a front end
* pacing itself on a blocking write to the sound card would then run at the wrong speed. Mesen
* holds its APU on the extra lines for exactly this reason, and so does this.
*
* <h2>Why the counter still moves</h2>
*
* {@link #cycles} is not only a count of work done; its <em>parity</em> is what
* {@link CPUBus#isGetCycle} reads, and two chips have to agree about it. The MMU asks that
* question of the CPU's counter when it starts a DMA cycle and this chip asks it of its own, and
* the two answers only match because both counters advance once per {@link 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.
* <p>
* A pending $4015 clear is honoured for the same reason: it is due on the next get cycle, and
* that cycle may well be one of these.
*
* <h2>What the phase costs</h2>
*
* A block of held cycles is a gap in the middle of the chip's sequences rather than a stretch of
* them, so the two one-cycle write delays -- {@code FrameCounter.writeDelay} and
* {@code DMC.loadDelay} -- land after the block rather than inside it, and a block of odd length
* moves which CPU cycle the pulse and noise dividers fall on by one. Neither is audible and
* neither accumulates: the divider keeps its own period, so what shifts is the phase of a
* waveform and not its frequency.
*/
public void idle() {
if (frameIRQClearPending && CPUBus.isGetCycle(cycles)) {
setFrameIRQFlag(false);
}

cycles++;
}

/**
* Takes this cycle's output into the running average, and finishes a sample when one falls
* due.
Expand Down Expand Up @@ -372,11 +415,13 @@ public int availableSamples() {
}

/**
* How many CPU cycles the chip has been clocked for since power on.
* How many CPU cycles the chip has been driven or held for since power on.
* <p>
* The APU's own clock, in the same sense that {@link PPU#getFrame()} is the PPU's: it is what
* says the chip is being driven at the rate it should be, including through the cycles an OAM
* DMA transfer holds the CPU off the bus.
* DMA transfer holds the CPU off the bus. "Or held", because an {@link #idle()} cycle counts
* here too -- this is the parity two chips agree on before it is a measure of work done, and it
* is what keeps {@code run.apuCycles} equal to {@code run.cpuCycles} however long a frame is.
*/
public long getCycles() {
return cycles;
Expand Down
14 changes: 13 additions & 1 deletion mynes-core/src/main/java/com/github/dimiro1/mynes/NES.java
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,13 @@ public void reset() {
* dots do: a $4015 read has to see the interrupt flag the frame counter raised in the cycle
* doing the reading. It is clocked here rather than from the CPU because it keeps running
* through an OAM DMA transfer, which the CPU spends held off the bus.
* <p>
* It is the one chip the {@link Overclock} hack holds still. A cycle the PPU spends on a line it
* is running again is a cycle the game gets and the sound does not: the APU keeps its parity,
* which two chips have to agree on, and counts nothing else -- so an overclocked frame is longer
* for the program and exactly as long as a hardware one for the music. {@link APU#idle()} has
* the whole of why. Asked once per CPU cycle, which on a machine nobody is overclocking is one
* boolean read.
*
* @see CPU#sampleNMI()
*/
Expand All @@ -127,7 +134,12 @@ public void tick() {
ppu.tick();
}

apu.tick();
if (ppu.isOnExtraLine()) {
apu.idle();
} else {
apu.tick();
}

cpu.tick();
}

Expand Down
Loading