diff --git a/Core/NES/NesPpu.cpp b/Core/NES/NesPpu.cpp index 553600ee1..93af3b616 100644 --- a/Core/NES/NesPpu.cpp +++ b/Core/NES/NesPpu.cpp @@ -950,6 +950,47 @@ template void NesPpu::ProcessScanlineImpl() } } +template void NesPpu::ProcessSpriteEvaluationStart() +{ + _sprite0Added = false; + _spriteInRange = false; + _secondaryOamAddr = 0; + + _overflowBugCounter = 0; + + _oamCopyDone = false; + + //Sprite evaluation does not necessarily start on the first byte of OAM + //it can start on any byte (based on the OAM address in 2003), and interprets + //that byte as the "sprite 0" Y coordinate. + _spriteAddrH = (_spriteRamAddr >> 2) & 0x3F; + _spriteAddrL = _spriteRamAddr & 0x03; + + _firstVisibleSpriteAddr = _spriteAddrH * 4; + _lastVisibleSpriteAddr = _firstVisibleSpriteAddr; +} + +template void NesPpu::ProcessSpriteEvaluationEnd() +{ + _sprite0Visible = _sprite0Added; + _spriteCount = (_secondaryOamAddr >> 2); + + if(_settings->GetNesConfig().EnablePpuSpriteEvalBug) { + //(Not entirely confirmed - but matches observed behavior) + //For early PPUs (2C02B and earlier), after sprite eval wraps back to the start of OAM, + //all subsequent sprites appear to be considered as "out of range", causing only their + //Y coordinate to be copied to secondary OAM, and then skipping to the next sprite. + //However, if the last Y position copied to secondary OAM by this process happens to be + //"in range", it will be end up being shown as a sprite. The sprite's remaining 3 bytes + //will be $FF (because secondary OAM was cleared at the start of the scanline), causing + //it to display pixels from sprite tile $FF at X=255, with h+v mirroring and sprite palette 3. + bool inRange = (_scanline >= _oamCopybuffer && _scanline < _oamCopybuffer + (_control.LargeSprites ? 16 : 8)); + if(inRange && _spriteCount < 8) { + _spriteCount++; + } + } +} + template void NesPpu::ProcessSpriteEvaluation() { if(IsRenderingEnabled() || (_region == ConsoleRegion::Pal && _scanline >= _palSpriteEvalScanline)) { @@ -958,33 +999,19 @@ template void NesPpu::ProcessSpriteEvaluation() _oamCopybuffer = 0xFF; _secondarySpriteRam[(_cycle - 1) >> 1] = 0xFF; } else { - if(_cycle == 65) { - _sprite0Added = false; - _spriteInRange = false; - _secondaryOamAddr = 0; - - _overflowBugCounter = 0; - - _oamCopyDone = false; - - //Sprite evaluation does not necessarily start on the first byte of OAM - //it can start on any byte (based on the OAM address in 2003), and interprets - //that byte as the "sprite 0" Y coordinate. - _spriteAddrH = (_spriteRamAddr >> 2) & 0x3F; - _spriteAddrL = _spriteRamAddr & 0x03; - - _firstVisibleSpriteAddr = _spriteAddrH * 4; - _lastVisibleSpriteAddr = _firstVisibleSpriteAddr; - } else if(_cycle == 256) { - _sprite0Visible = _sprite0Added; - _spriteCount = (_secondaryOamAddr >> 2); - } - if(_cycle & 0x01) { + if(_cycle == 65) { + ProcessSpriteEvaluationStart(); + } + //Read a byte from the primary OAM on odd cycles _oamCopybuffer = ReadSpriteRam(_spriteRamAddr); } else { - if(_oamCopyDone) { + if(_cycle == 256) { + ProcessSpriteEvaluationEnd(); + } + + if(_oamCopyDone && !_settings->GetNesConfig().EnablePpuSpriteEvalBug) { _spriteAddrH = (_spriteAddrH + 1) & 0x3F; if(_secondaryOamAddr >= 0x20) { //"As seen above, a side effect of the OAM write disable signal is to turn writes to the secondary OAM into reads from it." @@ -992,7 +1019,7 @@ template void NesPpu::ProcessSpriteEvaluation() } } else { if(!_spriteInRange && _scanline >= _oamCopybuffer && _scanline < _oamCopybuffer + (_control.LargeSprites ? 16 : 8)) { - _spriteInRange = true; + _spriteInRange = !_oamCopyDone; } if(_secondaryOamAddr < 0x20) { @@ -1013,6 +1040,10 @@ template void NesPpu::ProcessSpriteEvaluation() if(_spriteAddrL >= 4) { _spriteAddrH = (_spriteAddrH + 1) & 0x3F; _spriteAddrL = 0; + + if(_spriteAddrH == 0) { + _oamCopyDone = true; + } } //Note: Using "(_secondaryOamAddr & 0x03) == 0" instead of "_spriteAddrL == 0" is required @@ -1021,6 +1052,7 @@ template void NesPpu::ProcessSpriteEvaluation() if((_secondaryOamAddr & 0x03) == 0) { //Done copying all 4 bytes _spriteInRange = false; + _lastVisibleSpriteAddr = _spriteAddrH * 4; if(_spriteAddrL != 0) { //Normally, if the sprite eval started on a non-multiple-of-4 address, it would @@ -1033,11 +1065,6 @@ template void NesPpu::ProcessSpriteEvaluation() _spriteAddrL = 0; } } - - _lastVisibleSpriteAddr = _spriteAddrH * 4; - if(_spriteAddrH == 0) { - _oamCopyDone = true; - } } } else { //Nothing to copy, skip to next sprite diff --git a/Core/NES/NesPpu.h b/Core/NES/NesPpu.h index 6a4abdb93..1c7e11d62 100644 --- a/Core/NES/NesPpu.h +++ b/Core/NES/NesPpu.h @@ -58,6 +58,8 @@ class NesPpu : public BaseNesPpu void ProcessScanlineFirstCycle(); __forceinline void ProcessScanlineImpl(); __forceinline void ProcessSpriteEvaluation(); + __noinline void ProcessSpriteEvaluationStart(); + __noinline void ProcessSpriteEvaluationEnd(); void BeginVBlank(); void TriggerNmi(); diff --git a/Core/Shared/EmuSettings.cpp b/Core/Shared/EmuSettings.cpp index c2ea82aa9..7553be136 100644 --- a/Core/Shared/EmuSettings.cpp +++ b/Core/Shared/EmuSettings.cpp @@ -58,6 +58,7 @@ void EmuSettings::Serialize(Serializer& s) SV(_nes.RestrictPpuAccessOnFirstFrame); SV(_nes.EnableCpuTestMode); SV(_nes.EnableDmcSampleDuplicationGlitch); + SV(_nes.EnablePpuSpriteEvalBug); SV(_nes.PpuExtraScanlinesAfterNmi); SV(_nes.PpuExtraScanlinesBeforeNmi); SV(_nes.Region); SV(_nes.LightDetectionRadius); diff --git a/Core/Shared/SettingTypes.h b/Core/Shared/SettingTypes.h index 6a071742f..2228ea5ac 100644 --- a/Core/Shared/SettingTypes.h +++ b/Core/Shared/SettingTypes.h @@ -635,6 +635,7 @@ struct NesConfig bool EnableOamDecay = false; bool EnablePpuOamRowCorruption = false; + bool EnablePpuSpriteEvalBug = false; bool DisableOamAddrBug = false; bool DisablePaletteRead = false; bool DisablePpu2004Reads = false; diff --git a/UI/Config/NesConfig.cs b/UI/Config/NesConfig.cs index d37fad796..9da860e4b 100644 --- a/UI/Config/NesConfig.cs +++ b/UI/Config/NesConfig.cs @@ -63,6 +63,7 @@ public class NesConfig : BaseConfig //Emulation [Reactive] public bool EnableOamDecay { get; set; } = false; [Reactive] public bool EnablePpuOamRowCorruption { get; set; } = false; + [Reactive] public bool EnablePpuSpriteEvalBug { get; set; } = false; [Reactive] public bool DisableOamAddrBug { get; set; } = false; [Reactive] public bool DisablePaletteRead { get; set; } = false; [Reactive] public bool DisablePpu2004Reads { get; set; } = false; @@ -186,6 +187,7 @@ public void ApplyConfig() EnableOamDecay = EnableOamDecay, EnablePpuOamRowCorruption = EnablePpuOamRowCorruption, + EnablePpuSpriteEvalBug = EnablePpuSpriteEvalBug, DisableOamAddrBug = DisableOamAddrBug, DisablePaletteRead = DisablePaletteRead, DisablePpu2004Reads = DisablePpu2004Reads, @@ -317,9 +319,10 @@ public struct InteropNesConfig [MarshalAs(UnmanagedType.I1)] public bool AllowInvalidInput; [MarshalAs(UnmanagedType.I1)] public bool DisableGameGenieBusConflicts; [MarshalAs(UnmanagedType.I1)] public bool DisableFlashSaves; - + [MarshalAs(UnmanagedType.I1)] public bool EnableOamDecay; [MarshalAs(UnmanagedType.I1)] public bool EnablePpuOamRowCorruption; + [MarshalAs(UnmanagedType.I1)] public bool EnablePpuSpriteEvalBug; [MarshalAs(UnmanagedType.I1)] public bool DisableOamAddrBug; [MarshalAs(UnmanagedType.I1)] public bool DisablePaletteRead; [MarshalAs(UnmanagedType.I1)] public bool DisablePpu2004Reads; diff --git a/UI/Localization/resources.en.xml b/UI/Localization/resources.en.xml index d24a91b3d..c2bcb2efb 100644 --- a/UI/Localization/resources.en.xml +++ b/UI/Localization/resources.en.xml @@ -333,7 +333,8 @@ Miscellaneous Settings Disable PPU OAMADDR bug emulation Disable PPU palette reads - Disable PPU $2004 reads (Famicom behavior) + Disable PPU $2004 reads + Enable erroneous sprite pixels at X=255 (2C02B and earlier) Do not reset PPU when resetting console (Famicom behavior) Disable Game Genie bus conflict emulation Disable flash saves (UNROM512 / GTROM / Rainbow) diff --git a/UI/Views/NesConfigView.axaml b/UI/Views/NesConfigView.axaml index b1c56bf6c..824485700 100644 --- a/UI/Views/NesConfigView.axaml +++ b/UI/Views/NesConfigView.axaml @@ -327,6 +327,7 @@ +