Skip to content

NES: Reimplement PAL forced OAM refresh according to new research#137

Merged
Fiskbit merged 4 commits into
masterfrom
fiskbit-pal-refresh
May 24, 2026
Merged

NES: Reimplement PAL forced OAM refresh according to new research#137
Fiskbit merged 4 commits into
masterfrom
fiskbit-pal-refresh

Conversation

@Fiskbit
Copy link
Copy Markdown
Member

@Fiskbit Fiskbit commented May 24, 2026

On PAL, vblank is so long that OAM may decay during it. The PPU works around this by incrementing the OAM1 address every odd dot (delayed by 1, so happening on the following even dot) during scanlines 265-310. The PPU reads from OAM1 on every dot during vblank/fblank, so by repeatedly visiting all the addresses, it reads every row and keeps OAM fresh.

When implementing this, I found that OAM was corrupting reliably. This is because the forced refresh normally leaves the OAM1 address set to $8C when rendering begins, which triggers the 2C02E+ OAM corruption bug. However, all 3 types of OAM corruption are actually fixed on PAL, so this change also restricts OAM corruption to the NTSC console region.

All tests continue to pass with this change. blargg's oam_read test fails as it should, but not as severely as it does on real hardware. I'm not clear on why, but certainly part of it is that this change allows OAM1 to be incremented twice on a single dot. Adding hacky support for limiting this to just 1 increment makes it fail a little more severely, but still not as bad as on hardware (and that change is out of scope for this commit). My best guess right now is that it has to do with the precise timing of the $2004 writes, as the OAM write and increment delays depend on the timing of the end of the CPU write.

On PAL, vblank is so long that OAM may decay during it. The PPU works around this by incrementing the OAM1 address every odd dot (delayed by 1, so happening on the following even dot) during scanlines 265-310. The PPU reads from OAM1 on every dot during vblank/fblank, so by repeatedly visiting all the addresses, it reads every row and keeps OAM fresh.

When implementing this, I found that OAM was corrupting reliably. This is because the forced refresh normally leaves the OAM1 address set to $8C when rendering begins, which triggers the 2C02E+ OAM corruption bug. However, all 3 types of OAM corruption are actually fixed on PAL, so this change also restricts OAM corruption to the NTSC console region.

All tests continue to pass with this change. blargg's oam_read test fails as it should, but not as severely as it does on real hardware. I'm not clear on why, but certainly part of it is that this change allows OAM1 to be incremented twice on a single dot. Adding hacky support for limiting this to just 1 increment makes it fail a little more severely, but still not as bad as on hardware (and that change is out of scope for this commit). My best guess right now is that it has to do with the precise timing of the $2004 writes, as the OAM write and increment delays depend on the timing of the end of the CPU write.
Fiskbit added 2 commits May 24, 2026 00:07
Based on my testing, frame-start (2C02E+ style) and mid-frame rendering toggle (NTSC style) OAM corruption happen on Dendy, but not $2003 write OAM corruption. So, this changes corruption emulation to match that.

Tested with oam_flicker_test, oam_flicker_test_reenable, oam_read, oam_stress, and Huge Insect.
@Fiskbit Fiskbit force-pushed the fiskbit-pal-refresh branch from ff9667c to e0ac5f1 Compare May 24, 2026 08:47
@Fiskbit
Copy link
Copy Markdown
Member Author

Fiskbit commented May 24, 2026

I did some Dendy testing on a UM6561AF-2. I did not detect anything that looks like forced OAM refresh anywhere in the frame, so it does not appear to be doing refresh between the start of vblank and NMI.

I did find that it is susceptible to 2 of the 3 sources of OAM corruption. $2003 writes do not corrupt, but rendering toggle or starting the frame with an OAM1/OAM2 row mismatch do corrupt. $2003 corruption was already limited to NTSC, but I've updated this change to only exempt PAL from the remaining corruption.

With these changes, MesenCE matches what I'm seeing on hardware for Dendy.

@Fiskbit Fiskbit merged commit 2d87f31 into master May 24, 2026
21 checks passed
@Fiskbit Fiskbit deleted the fiskbit-pal-refresh branch May 24, 2026 09:53
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.

2 participants