Mesen-X/Core/PPU.cpp
mkwong98 8e0816807f Add sprite frame range condition
A new condition named "spriteFrameRange" is added. It works like frameRange except it follows frame counters assigned to each sprite. Frame counter picks up the nearest frame counter within 6x6 pixels from the last frame and resets if graphics, palette, bg priority or orientation has changed. The condition can by used by sprite tiles only. For example:

<condition>myCondition,spriteFrameRange,301,60
[myCondition]<tile>0,1001,FF0F3600,8,0,1,N
2021-04-15 19:54:01 +08:00

1521 lines
No EOL
51 KiB
C++

#include "stdafx.h"
#include "PPU.h"
#include "CPU.h"
#include "APU.h"
#include "EmulationSettings.h"
#include "VideoDecoder.h"
#include "Debugger.h"
#include "BaseMapper.h"
#include "RewindManager.h"
#include "ControlManager.h"
#include "MemoryManager.h"
#include "Console.h"
#include "NotificationManager.h"
PPU::PPU(shared_ptr<Console> console)
{
_console = console;
_masterClock = 0;
_masterClockDivider = 4;
_settings = _console->GetSettings();
_outputBuffers[0] = new uint16_t[256 * 240];
_outputBuffers[1] = new uint16_t[256 * 240];
_currentOutputBuffer = _outputBuffers[0];
memset(_outputBuffers[0], 0, 256 * 240 * sizeof(uint16_t));
memset(_outputBuffers[1], 0, 256 * 240 * sizeof(uint16_t));
uint8_t paletteRamBootValues[0x20] { 0x09, 0x01, 0x00, 0x01, 0x00, 0x02, 0x02, 0x0D, 0x08, 0x10, 0x08, 0x24, 0x00, 0x00, 0x04, 0x2C,
0x09, 0x01, 0x34, 0x03, 0x00, 0x04, 0x00, 0x14, 0x08, 0x3A, 0x00, 0x02, 0x00, 0x20, 0x2C, 0x08 };
memcpy(_paletteRAM, paletteRamBootValues, sizeof(_paletteRAM));
//This should (presumably) persist across resets
memset(_corruptOamRow, 0, sizeof(_corruptOamRow));
_console->InitializeRam(_spriteRAM, 0x100);
_console->InitializeRam(_secondarySpriteRAM, 0x20);
Reset();
}
PPU::~PPU()
{
delete[] _outputBuffers[0];
delete[] _outputBuffers[1];
}
void PPU::Reset()
{
_masterClock = 0;
_preventVblFlag = false;
_needStateUpdate = false;
_prevRenderingEnabled = false;
_renderingEnabled = false;
_ignoreVramRead = 0;
_openBus = 0;
memset(_openBusDecayStamp, 0, sizeof(_openBusDecayStamp));
_state = {};
_flags = {};
_statusFlags = {};
_previousTile = {};
_currentTile = {};
_nextTile = {};
_ppuBusAddress = 0;
_intensifyColorBits = 0;
_paletteRamMask = 0x3F;
_lastUpdatedPixel = -1;
_lastSprite = nullptr;
_oamCopybuffer = 0;
_spriteInRange = false;
_sprite0Added = false;
_spriteAddrH = 0;
_spriteAddrL = 0;
_oamCopyDone = false;
_renderingEnabled = false;
_prevRenderingEnabled = false;
memset(_hasSprite, 0, sizeof(_hasSprite));
memset(_spriteTiles, 0, sizeof(_spriteTiles));
_spriteCount = 0;
_secondaryOAMAddr = 0;
_sprite0Visible = false;
_spriteIndex = 0;
_openBus = 0;
memset(_openBusDecayStamp, 0, sizeof(_openBusDecayStamp));
_ignoreVramRead = 0;
//First execution will be cycle 0, scanline 0
_scanline = -1;
_cycle = 340;
_frameCount = 1;
_memoryReadBuffer = 0;
_overflowBugCounter = 0;
_updateVramAddrDelay = 0;
_updateVramAddr = 0;
memset(_oamDecayCycles, 0, sizeof(_oamDecayCycles));
_enableOamDecay = _settings->CheckFlag(EmulationFlags::EnableOamDecay);
UpdateMinimumDrawCycles();
}
void PPU::SetNesModel(NesModel model)
{
_nesModel = model;
switch(_nesModel) {
case NesModel::Auto:
//Should never be Auto
break;
case NesModel::NTSC:
_nmiScanline = 241;
_vblankEnd = 260;
_standardNmiScanline = 241;
_standardVblankEnd = 260;
_masterClockDivider = 4;
break;
case NesModel::PAL:
_nmiScanline = 241;
_vblankEnd = 310;
_standardNmiScanline = 241;
_standardVblankEnd = 310;
_masterClockDivider = 5;
break;
case NesModel::Dendy:
_nmiScanline = 291;
_vblankEnd = 310;
_standardNmiScanline = 291;
_standardVblankEnd = 310;
_masterClockDivider = 5;
break;
}
_nmiScanline += _settings->GetPpuExtraScanlinesBeforeNmi();
_palSpriteEvalScanline = _nmiScanline + 24;
_standardVblankEnd += _settings->GetPpuExtraScanlinesBeforeNmi();
_vblankEnd += _settings->GetPpuExtraScanlinesAfterNmi() + _settings->GetPpuExtraScanlinesBeforeNmi();
}
double PPU::GetOverclockRate()
{
uint32_t regularVblankEnd;
switch(_nesModel) {
default:
case NesModel::NTSC: regularVblankEnd = 260; break;
case NesModel::PAL: regularVblankEnd = 310; break;
case NesModel::Dendy: regularVblankEnd = 310; break;
}
return (double)(_vblankEnd + 2) / (regularVblankEnd + 2);
}
void PPU::GetState(PPUDebugState &state)
{
state.ControlFlags = _flags;
state.StatusFlags = _statusFlags;
state.State = _state;
state.Cycle = _cycle;
state.Scanline = _scanline;
state.FrameCount = _frameCount;
state.NmiScanline = _nmiScanline;
state.ScanlineCount = _vblankEnd + 2;
state.SafeOamScanline = _nesModel == NesModel::NTSC ? _nmiScanline + 19 : _palSpriteEvalScanline;
state.BusAddress = _ppuBusAddress;
state.MemoryReadBuffer = _memoryReadBuffer;
}
void PPU::SetState(PPUDebugState &state)
{
_flags = state.ControlFlags;
_statusFlags = state.StatusFlags;
_state = state.State;
_cycle = state.Cycle;
_scanline = state.Scanline;
_frameCount = state.FrameCount;
UpdateMinimumDrawCycles();
_paletteRamMask = _flags.Grayscale ? 0x30 : 0x3F;
if(_nesModel == NesModel::NTSC) {
_intensifyColorBits = (_flags.IntensifyGreen ? 0x40 : 0x00) | (_flags.IntensifyRed ? 0x80 : 0x00) | (_flags.IntensifyBlue ? 0x100 : 0x00);
} else if(_nesModel == NesModel::PAL || _nesModel == NesModel::Dendy) {
//"Note that on the Dendy and PAL NES, the green and red bits swap meaning."
_intensifyColorBits = (_flags.IntensifyRed ? 0x40 : 0x00) | (_flags.IntensifyGreen ? 0x80 : 0x00) | (_flags.IntensifyBlue ? 0x100 : 0x00);
}
}
void PPU::UpdateVideoRamAddr()
{
if(_scanline >= 240 || !IsRenderingEnabled()) {
_state.VideoRamAddr = (_state.VideoRamAddr + (_flags.VerticalWrite ? 32 : 1)) & 0x7FFF;
if(!_renderingEnabled) {
_console->DebugAddDebugEvent(DebugEventType::BgColorChange);
}
//Trigger memory read when setting the vram address - needed by MMC3 IRQ counter
//"Should be clocked when A12 changes to 1 via $2007 read/write"
SetBusAddress(_state.VideoRamAddr & 0x3FFF);
} else {
//"During rendering (on the pre-render line and the visible lines 0-239, provided either background or sprite rendering is enabled), "
//it will update v in an odd way, triggering a coarse X increment and a Y increment simultaneously"
IncHorizontalScrolling();
IncVerticalScrolling();
}
}
void PPU::SetOpenBus(uint8_t mask, uint8_t value)
{
//Decay expired bits, set new bits and update stamps on each individual bit
if(mask == 0xFF) {
//Shortcut when mask is 0xFF - all bits are set to the value and stamps updated
_openBus = value;
for(int i = 0; i < 8; i++) {
_openBusDecayStamp[i] = _frameCount;
}
} else {
uint16_t openBus = (_openBus << 8);
for(int i = 0; i < 8; i++) {
openBus >>= 1;
if(mask & 0x01) {
if(value & 0x01) {
openBus |= 0x80;
} else {
openBus &= 0xFF7F;
}
_openBusDecayStamp[i] = _frameCount;
} else if(_frameCount - _openBusDecayStamp[i] > 30) {
openBus &= 0xFF7F;
}
value >>= 1;
mask >>= 1;
}
_openBus = (uint8_t)openBus;
}
}
uint8_t PPU::ApplyOpenBus(uint8_t mask, uint8_t value)
{
SetOpenBus(~mask, value);
return value | (_openBus & mask);
}
void PPU::ProcessStatusRegOpenBus(uint8_t &openBusMask, uint8_t &returnValue)
{
switch(_settings->GetPpuModel()) {
case PpuModel::Ppu2C05A: openBusMask = 0x00; returnValue |= 0x1B; break;
case PpuModel::Ppu2C05B: openBusMask = 0x00; returnValue |= 0x3D; break;
case PpuModel::Ppu2C05C: openBusMask = 0x00; returnValue |= 0x1C; break;
case PpuModel::Ppu2C05D: openBusMask = 0x00; returnValue |= 0x1B; break;
case PpuModel::Ppu2C05E: openBusMask = 0x00; break;
default: break;
}
}
uint8_t PPU::PeekRAM(uint16_t addr)
{
//Used by debugger to get register values without side-effects (heavily edited copy of ReadRAM)
uint8_t openBusMask = 0xFF;
uint8_t returnValue = 0;
switch(GetRegisterID(addr)) {
case PPURegisters::Status:
returnValue = ((uint8_t)_statusFlags.SpriteOverflow << 5) | ((uint8_t)_statusFlags.Sprite0Hit << 6) | ((uint8_t)_statusFlags.VerticalBlank << 7);
if(_scanline == _nmiScanline && _cycle < 3) {
//Clear vertical blank flag
returnValue &= 0x7F;
}
openBusMask = 0x1F;
ProcessStatusRegOpenBus(openBusMask, returnValue);
break;
case PPURegisters::SpriteData:
if(!_settings->CheckFlag(EmulationFlags::DisablePpu2004Reads)) {
if(_scanline <= 239 && IsRenderingEnabled()) {
if(_cycle >= 257 && _cycle <= 320) {
uint8_t step = ((_cycle - 257) % 8) > 3 ? 3 : ((_cycle - 257) % 8);
uint8_t addr = (_cycle - 257) / 8 * 4 + step;
returnValue = _secondarySpriteRAM[addr];
} else {
returnValue = _oamCopybuffer;
}
} else {
returnValue = _spriteRAM[_state.SpriteRamAddr];
}
openBusMask = 0x00;
}
break;
case PPURegisters::VideoMemoryData:
returnValue = _memoryReadBuffer;
if((_state.VideoRamAddr & 0x3FFF) >= 0x3F00 && !_settings->CheckFlag(EmulationFlags::DisablePaletteRead)) {
returnValue = ReadPaletteRAM(_state.VideoRamAddr) | (_openBus & 0xC0);
openBusMask = 0xC0;
} else {
openBusMask = 0x00;
}
break;
default:
break;
}
return returnValue | (_openBus & openBusMask);
}
uint8_t PPU::ReadRAM(uint16_t addr)
{
uint8_t openBusMask = 0xFF;
uint8_t returnValue = 0;
switch(GetRegisterID(addr)) {
case PPURegisters::Status:
_state.WriteToggle = false;
UpdateStatusFlag();
returnValue = _state.Status;
openBusMask = 0x1F;
ProcessStatusRegOpenBus(openBusMask, returnValue);
break;
case PPURegisters::SpriteData:
if(!_settings->CheckFlag(EmulationFlags::DisablePpu2004Reads)) {
if(_scanline <= 239 && IsRenderingEnabled()) {
//While the screen is begin drawn
if(_cycle >= 257 && _cycle <= 320) {
//If we're doing sprite rendering, set OAM copy buffer to its proper value. This is done here for performance.
//It's faster to only do this here when it's needed, rather than splitting LoadSpriteTileInfo() into an 8-step process
uint8_t step = ((_cycle - 257) % 8) > 3 ? 3 : ((_cycle - 257) % 8);
_secondaryOAMAddr = (_cycle - 257) / 8 * 4 + step;
_oamCopybuffer = _secondarySpriteRAM[_secondaryOAMAddr];
_oamID = _secondarySpriteRAMLink[_secondaryOAMAddr >> 2];
}
//Return the value that PPU is currently using for sprite evaluation/rendering
returnValue = _oamCopybuffer;
} else {
returnValue = ReadSpriteRam(_state.SpriteRamAddr);
}
openBusMask = 0x00;
}
break;
case PPURegisters::VideoMemoryData:
if(_ignoreVramRead) {
//2 reads to $2007 in quick succession (2 consecutive CPU cycles) causes the 2nd read to be ignored (normally depends on PPU/CPU timing, but this is the simplest solution)
//Return open bus in this case? (which will match the last value read)
openBusMask = 0xFF;
} else {
returnValue = _memoryReadBuffer;
_memoryReadBuffer = ReadVram(_ppuBusAddress & 0x3FFF, MemoryOperationType::Read);
if((_ppuBusAddress & 0x3FFF) >= 0x3F00 && !_settings->CheckFlag(EmulationFlags::DisablePaletteRead)) {
returnValue = ReadPaletteRAM(_ppuBusAddress) | (_openBus & 0xC0);
_console->DebugProcessVramReadOperation(MemoryOperationType::Read, _ppuBusAddress & 0x3FFF, returnValue);
openBusMask = 0xC0;
} else {
openBusMask = 0x00;
}
UpdateVideoRamAddr();
_ignoreVramRead = 6;
_needStateUpdate = true;
}
break;
default:
break;
}
return ApplyOpenBus(openBusMask, returnValue);
}
void PPU::WriteRAM(uint16_t addr, uint8_t value)
{
if(addr != 0x4014) {
SetOpenBus(0xFF, value);
}
switch(GetRegisterID(addr)) {
case PPURegisters::Control:
if(_settings->GetPpuModel() >= PpuModel::Ppu2C05A && _settings->GetPpuModel() <= PpuModel::Ppu2C05E) {
SetMaskRegister(value);
} else {
SetControlRegister(value);
}
break;
case PPURegisters::Mask:
if(_settings->GetPpuModel() >= PpuModel::Ppu2C05A && _settings->GetPpuModel() <= PpuModel::Ppu2C05E) {
SetControlRegister(value);
} else {
SetMaskRegister(value);
}
break;
case PPURegisters::SpriteAddr:
_state.SpriteRamAddr = value;
break;
case PPURegisters::SpriteData:
if((_scanline >= 240 && (_nesModel != NesModel::PAL || _scanline < _palSpriteEvalScanline)) || !IsRenderingEnabled()) {
if((_state.SpriteRamAddr & 0x03) == 0x02) {
//"The three unimplemented bits of each sprite's byte 2 do not exist in the PPU and always read back as 0 on PPU revisions that allow reading PPU OAM through OAMDATA ($2004)"
value &= 0xE3;
}
WriteSpriteRam(_state.SpriteRamAddr, value);
_state.SpriteRamAddr = (_state.SpriteRamAddr + 1) & 0xFF;
} else {
//"Writes to OAMDATA during rendering (on the pre-render line and the visible lines 0-239, provided either sprite or background rendering is enabled) do not modify values in OAM,
//but do perform a glitchy increment of OAMADDR, bumping only the high 6 bits"
_state.SpriteRamAddr = (_state.SpriteRamAddr + 4) & 0xFF;
}
break;
case PPURegisters::ScrollOffsets:
if(_state.WriteToggle) {
_state.TmpVideoRamAddr = (_state.TmpVideoRamAddr & ~0x73E0) | ((value & 0xF8) << 2) | ((value & 0x07) << 12);
} else {
_state.XScroll = value & 0x07;
uint16_t newAddr = (_state.TmpVideoRamAddr & ~0x001F) | (value >> 3);
ProcessTmpAddrScrollGlitch(newAddr, _console->GetMemoryManager()->GetOpenBus() >> 3, 0x001F);
}
_state.WriteToggle = !_state.WriteToggle;
break;
case PPURegisters::VideoMemoryAddr:
if(_state.WriteToggle) {
_state.TmpVideoRamAddr = (_state.TmpVideoRamAddr & ~0x00FF) | value;
//Video RAM update is apparently delayed by 3 PPU cycles (based on Visual NES findings)
_needStateUpdate = true;
_updateVramAddrDelay = 3;
_updateVramAddr = _state.TmpVideoRamAddr;
_console->DebugSetLastFramePpuScroll(_updateVramAddr, _state.XScroll, false);
} else {
uint16_t newAddr = (_state.TmpVideoRamAddr & ~0xFF00) | ((value & 0x3F) << 8);
ProcessTmpAddrScrollGlitch(newAddr, _console->GetMemoryManager()->GetOpenBus() << 8, 0x0C00);
}
_state.WriteToggle = !_state.WriteToggle;
break;
case PPURegisters::VideoMemoryData:
if((_ppuBusAddress & 0x3FFF) >= 0x3F00) {
WritePaletteRAM(_ppuBusAddress, value);
_console->DebugProcessVramWriteOperation(_ppuBusAddress & 0x3FFF, value);
} else {
if(_scanline >= 240 || !IsRenderingEnabled()) {
_console->GetMapper()->WriteVRAM(_ppuBusAddress & 0x3FFF, value);
} else {
//During rendering, the value written is ignored, and instead the address' LSB is used (not confirmed, based on Visual NES)
_console->GetMapper()->WriteVRAM(_ppuBusAddress & 0x3FFF, _ppuBusAddress & 0xFF);
}
}
UpdateVideoRamAddr();
break;
case PPURegisters::SpriteDMA:
_console->GetCpu()->RunDMATransfer(value);
break;
default:
break;
}
}
uint8_t PPU::ReadPaletteRAM(uint16_t addr)
{
addr &= 0x1F;
if(addr == 0x10 || addr == 0x14 || addr == 0x18 || addr == 0x1C) {
addr &= ~0x10;
}
return _paletteRAM[addr];
}
void PPU::WritePaletteRAM(uint16_t addr, uint8_t value)
{
addr &= 0x1F;
value &= 0x3F;
if(addr == 0x00 || addr == 0x10) {
_paletteRAM[0x00] = value;
_paletteRAM[0x10] = value;
} else if(addr == 0x04 || addr == 0x14) {
_paletteRAM[0x04] = value;
_paletteRAM[0x14] = value;
} else if(addr == 0x08 || addr == 0x18) {
_paletteRAM[0x08] = value;
_paletteRAM[0x18] = value;
} else if(addr == 0x0C || addr == 0x1C) {
_paletteRAM[0x0C] = value;
_paletteRAM[0x1C] = value;
} else {
_paletteRAM[addr] = value;
}
}
bool PPU::IsRenderingEnabled()
{
return _renderingEnabled;
}
void PPU::ProcessTmpAddrScrollGlitch(uint16_t normalAddr, uint16_t value, uint16_t mask)
{
_state.TmpVideoRamAddr = normalAddr;
if(_cycle == 257 && _settings->CheckFlag(EmulationFlags::EnablePpu2000ScrollGlitch) && _scanline < 240 && IsRenderingEnabled()) {
//Use open bus to set some parts of V (glitch that occurs when writing to $2000/$2005/$2006 on cycle 257)
_state.VideoRamAddr = (_state.VideoRamAddr & ~mask) | (value & mask);
}
}
void PPU::SetControlRegister(uint8_t value)
{
_state.Control = value;
uint8_t nameTable = (_state.Control & 0x03);
uint16_t normalAddr = (_state.TmpVideoRamAddr & ~0x0C00) | (nameTable << 10);
ProcessTmpAddrScrollGlitch(normalAddr, _console->GetMemoryManager()->GetOpenBus() << 10, 0x0400);
_flags.VerticalWrite = (_state.Control & 0x04) == 0x04;
_flags.SpritePatternAddr = ((_state.Control & 0x08) == 0x08) ? 0x1000 : 0x0000;
_flags.BackgroundPatternAddr = ((_state.Control & 0x10) == 0x10) ? 0x1000 : 0x0000;
_flags.LargeSprites = (_state.Control & 0x20) == 0x20;
_flags.VBlank = (_state.Control & 0x80) == 0x80;
//"By toggling NMI_output ($2000 bit 7) during vertical blank without reading $2002, a program can cause /NMI to be pulled low multiple times, causing multiple NMIs to be generated."
if(!_flags.VBlank) {
_console->GetCpu()->ClearNmiFlag();
} else if(_flags.VBlank && _statusFlags.VerticalBlank) {
_console->GetCpu()->SetNmiFlag();
}
}
void PPU::UpdateMinimumDrawCycles()
{
_minimumDrawBgCycle = _flags.BackgroundEnabled ? ((_flags.BackgroundMask || _settings->CheckFlag(EmulationFlags::ForceBackgroundFirstColumn)) ? 0 : 8) : 300;
_minimumDrawSpriteCycle = _flags.SpritesEnabled ? ((_flags.SpriteMask || _settings->CheckFlag(EmulationFlags::ForceSpritesFirstColumn)) ? 0 : 8) : 300;
_minimumDrawSpriteStandardCycle = _flags.SpritesEnabled ? (_flags.SpriteMask ? 0 : 8) : 300;
}
void PPU::SetMaskRegister(uint8_t value)
{
_state.Mask = value;
_flags.Grayscale = (_state.Mask & 0x01) == 0x01;
_flags.BackgroundMask = (_state.Mask & 0x02) == 0x02;
_flags.SpriteMask = (_state.Mask & 0x04) == 0x04;
_flags.BackgroundEnabled = (_state.Mask & 0x08) == 0x08;
_flags.SpritesEnabled = (_state.Mask & 0x10) == 0x10;
_flags.IntensifyBlue = (_state.Mask & 0x80) == 0x80;
if(_renderingEnabled != (_flags.BackgroundEnabled | _flags.SpritesEnabled)) {
_needStateUpdate = true;
}
UpdateMinimumDrawCycles();
UpdateGrayscaleAndIntensifyBits();
//"Bit 0 controls a greyscale mode, which causes the palette to use only the colors from the grey column: $00, $10, $20, $30. This is implemented as a bitwise AND with $30 on any value read from PPU $3F00-$3FFF"
_paletteRamMask = _flags.Grayscale ? 0x30 : 0x3F;
if(_nesModel == NesModel::NTSC) {
_flags.IntensifyRed = (_state.Mask & 0x20) == 0x20;
_flags.IntensifyGreen = (_state.Mask & 0x40) == 0x40;
_intensifyColorBits = (value & 0xE0) << 1;
} else if(_nesModel == NesModel::PAL || _nesModel == NesModel::Dendy) {
//"Note that on the Dendy and PAL NES, the green and red bits swap meaning."
_flags.IntensifyRed = (_state.Mask & 0x40) == 0x40;
_flags.IntensifyGreen = (_state.Mask & 0x20) == 0x20;
_intensifyColorBits = (_flags.IntensifyRed ? 0x40 : 0x00) | (_flags.IntensifyGreen ? 0x80 : 0x00) | (_flags.IntensifyBlue ? 0x100 : 0x00);
}
_console->DebugAddDebugEvent(DebugEventType::BgColorChange);
}
void PPU::UpdateStatusFlag()
{
_state.Status = ((uint8_t)_statusFlags.SpriteOverflow << 5) |
((uint8_t)_statusFlags.Sprite0Hit << 6) |
((uint8_t)_statusFlags.VerticalBlank << 7);
_statusFlags.VerticalBlank = false;
_console->GetCpu()->ClearNmiFlag();
if(_scanline == _nmiScanline && _cycle == 0) {
//"Reading one PPU clock before reads it as clear and never sets the flag or generates NMI for that frame."
_preventVblFlag = true;
}
}
//Taken from http://wiki.nesdev.com/w/index.php/The_skinny_on_NES_scrolling#Wrapping_around
void PPU::IncVerticalScrolling()
{
uint16_t addr = _state.VideoRamAddr;
if((addr & 0x7000) != 0x7000) {
// if fine Y < 7
addr += 0x1000; // increment fine Y
} else {
// fine Y = 0
addr &= ~0x7000;
int y = (addr & 0x03E0) >> 5; // let y = coarse Y
if(y == 29) {
y = 0; // coarse Y = 0
addr ^= 0x0800; // switch vertical nametable
} else if(y == 31){
y = 0; // coarse Y = 0, nametable not switched
} else {
y++; // increment coarse Y
}
addr = (addr & ~0x03E0) | (y << 5); // put coarse Y back into v
}
_state.VideoRamAddr = addr;
}
//Taken from http://wiki.nesdev.com/w/index.php/The_skinny_on_NES_scrolling#Wrapping_around
void PPU::IncHorizontalScrolling()
{
//Increase coarse X scrolling value.
uint16_t addr = _state.VideoRamAddr;
if((addr & 0x001F) == 31) {
//When the value is 31, wrap around to 0 and switch nametable
addr = (addr & ~0x001F) ^ 0x0400;
} else {
addr++;
}
_state.VideoRamAddr = addr;
}
//Taken from http://wiki.nesdev.com/w/index.php/The_skinny_on_NES_scrolling#Tile_and_attribute_fetching
uint16_t PPU::GetNameTableAddr()
{
return 0x2000 | (_state.VideoRamAddr & 0x0FFF);
}
//Taken from http://wiki.nesdev.com/w/index.php/The_skinny_on_NES_scrolling#Tile_and_attribute_fetching
uint16_t PPU::GetAttributeAddr()
{
return 0x23C0 | (_state.VideoRamAddr & 0x0C00) | ((_state.VideoRamAddr >> 4) & 0x38) | ((_state.VideoRamAddr >> 2) & 0x07);
}
void PPU::SetBusAddress(uint16_t addr)
{
_ppuBusAddress = addr;
_console->GetMapper()->NotifyVRAMAddressChange(addr);
}
uint8_t PPU::ReadVram(uint16_t addr, MemoryOperationType type)
{
SetBusAddress(addr);
return _console->GetMapper()->ReadVRAM(addr, type);
}
void PPU::WriteVram(uint16_t addr, uint8_t value)
{
SetBusAddress(addr);
_console->GetMapper()->WriteVRAM(addr, value);
}
void PPU::LoadTileInfo()
{
if(IsRenderingEnabled()) {
switch(_cycle & 0x07) {
case 1: {
_previousTile = _currentTile;
_currentTile = _nextTile;
_state.LowBitShift |= _nextTile.LowByte;
_state.HighBitShift |= _nextTile.HighByte;
uint8_t tileIndex = ReadVram(GetNameTableAddr());
_nextTile.TileAddr = (tileIndex << 4) | (_state.VideoRamAddr >> 12) | _flags.BackgroundPatternAddr;
_nextTile.OffsetY = _state.VideoRamAddr >> 12;
break;
}
case 3: {
uint8_t shift = ((_state.VideoRamAddr >> 4) & 0x04) | (_state.VideoRamAddr & 0x02);
_nextTile.PaletteOffset = ((ReadVram(GetAttributeAddr()) >> shift) & 0x03) << 2;
break;
}
case 5:
_nextTile.LowByte = ReadVram(_nextTile.TileAddr);
_nextTile.AbsoluteTileAddr = _console->GetMapper()->ToAbsoluteChrAddress(_nextTile.TileAddr);
break;
case 7:
_nextTile.HighByte = ReadVram(_nextTile.TileAddr + 8);
break;
}
}
}
void PPU::LoadSprite(uint8_t spriteY, uint8_t tileIndex, uint8_t attributes, uint8_t spriteX, bool extraSprite, uint8_t oamID)
{
bool backgroundPriority = (attributes & 0x20) == 0x20;
bool horizontalMirror = (attributes & 0x40) == 0x40;
bool verticalMirror = (attributes & 0x80) == 0x80;
uint16_t tileAddr;
uint8_t lineOffset;
if(verticalMirror) {
lineOffset = (_flags.LargeSprites ? 15 : 7) - (_scanline - spriteY);
} else {
lineOffset = _scanline - spriteY;
}
if(_flags.LargeSprites) {
tileAddr = (((tileIndex & 0x01) ? 0x1000 : 0x0000) | ((tileIndex & ~0x01) << 4)) + (lineOffset >= 8 ? lineOffset + 8 : lineOffset);
} else {
tileAddr = ((tileIndex << 4) | _flags.SpritePatternAddr) + lineOffset;
}
bool fetchLastSprite = true;
if((_spriteIndex < _spriteCount || extraSprite) && spriteY < 240) {
SpriteInfo &info = _spriteTiles[_spriteIndex];
info.BackgroundPriority = backgroundPriority;
info.HorizontalMirror = horizontalMirror;
info.VerticalMirror = verticalMirror;
info.PaletteOffset = ((attributes & 0x03) << 2) | 0x10;
if(extraSprite) {
//Use DebugReadVRAM for extra sprites to prevent side-effects.
info.LowByte = _console->GetMapper()->DebugReadVRAM(tileAddr);
info.HighByte = _console->GetMapper()->DebugReadVRAM(tileAddr + 8);
} else {
fetchLastSprite = false;
info.LowByte = ReadVram(tileAddr);
info.HighByte = ReadVram(tileAddr + 8);
}
info.TileAddr = tileAddr;
info.AbsoluteTileAddr = _console->GetMapper()->ToAbsoluteChrAddress(tileAddr);
info.OffsetY = lineOffset;
info.SpriteX = spriteX;
info.OAMIndex = oamID;
if(_scanline >= 0) {
//Sprites read on prerender scanline are not shown on scanline 0
for(int i = 0; i < 8 && spriteX + i + 1 < 257; i++) {
_hasSprite[spriteX + i + 1] = true;
}
}
}
if(fetchLastSprite) {
//Fetches to sprite 0xFF for remaining sprites/hidden - used by MMC3 IRQ counter
lineOffset = 0;
tileIndex = 0xFF;
if(_flags.LargeSprites) {
tileAddr = (((tileIndex & 0x01) ? 0x1000 : 0x0000) | ((tileIndex & ~0x01) << 4)) + (lineOffset >= 8 ? lineOffset + 8 : lineOffset);
} else {
tileAddr = ((tileIndex << 4) | _flags.SpritePatternAddr) + lineOffset;
}
ReadVram(tileAddr);
ReadVram(tileAddr + 8);
}
_spriteIndex++;
}
void PPU::LoadExtraSprites()
{
if(_spriteCount == 8 && _settings->CheckFlag(EmulationFlags::RemoveSpriteLimit)) {
bool loadExtraSprites = true;
if(_settings->CheckFlag(EmulationFlags::AdaptiveSpriteLimit)) {
uint16_t lastPosition = 0xFFFF;
uint8_t identicalSpriteCount = 0;
uint8_t maxIdenticalSpriteCount = 0;
for(int i = 0; i < 64; i++) {
uint8_t y = _spriteRAM[i << 2];
if(_scanline >= y && _scanline < y + (_flags.LargeSprites ? 16 : 8)) {
uint8_t x = _spriteRAM[(i << 2) + 3];
uint16_t position = (y << 8) | x;
if(lastPosition != position) {
if(identicalSpriteCount > maxIdenticalSpriteCount) {
maxIdenticalSpriteCount = identicalSpriteCount;
}
lastPosition = position;
identicalSpriteCount = 1;
} else {
identicalSpriteCount++;
}
}
}
loadExtraSprites = identicalSpriteCount < 8 && maxIdenticalSpriteCount < 8;
}
if(loadExtraSprites) {
for(uint32_t i = (_lastVisibleSpriteAddr + 4) & 0xFF; i != _firstVisibleSpriteAddr; i = (i + 4) & 0xFF) {
uint8_t spriteY = _spriteRAM[i];
if(_scanline >= spriteY && _scanline < spriteY + (_flags.LargeSprites ? 16 : 8)) {
LoadSprite(spriteY, _spriteRAM[i + 1], _spriteRAM[i + 2], _spriteRAM[i + 3], true, i >> 2);
_spriteCount++;
}
}
}
}
}
void PPU::LoadSpriteTileInfo()
{
uint8_t *spriteAddr = _secondarySpriteRAM + _spriteIndex * 4;
LoadSprite(*spriteAddr, *(spriteAddr+1), *(spriteAddr+2), *(spriteAddr+3), false, _secondarySpriteRAMLink[_spriteIndex]);
}
void PPU::ShiftTileRegisters()
{
_state.LowBitShift <<= 1;
_state.HighBitShift <<= 1;
}
uint8_t PPU::GetPixelColor()
{
uint8_t offset = _state.XScroll;
uint8_t backgroundColor = 0;
uint8_t spriteBgColor = 0;
if(_cycle > _minimumDrawBgCycle) {
//BackgroundMask = false: Hide background in leftmost 8 pixels of screen
spriteBgColor = (((_state.LowBitShift << offset) & 0x8000) >> 15) | (((_state.HighBitShift << offset) & 0x8000) >> 14);
if(_settings->GetBackgroundEnabled()) {
backgroundColor = spriteBgColor;
}
}
if(_hasSprite[_cycle] && _cycle > _minimumDrawSpriteCycle) {
//SpriteMask = true: Hide sprites in leftmost 8 pixels of screen
for(uint8_t i = 0; i < _spriteCount; i++) {
int32_t shift = (int32_t)_cycle - _spriteTiles[i].SpriteX - 1;
if(shift >= 0 && shift < 8) {
_lastSprite = &_spriteTiles[i];
uint8_t spriteColor;
if(_spriteTiles[i].HorizontalMirror) {
spriteColor = ((_lastSprite->LowByte >> shift) & 0x01) | ((_lastSprite->HighByte >> shift) & 0x01) << 1;
} else {
spriteColor = ((_lastSprite->LowByte << shift) & 0x80) >> 7 | ((_lastSprite->HighByte << shift) & 0x80) >> 6;
}
if(spriteColor != 0) {
//First sprite without a 00 color, use it.
if(i == 0 && spriteBgColor != 0 && _sprite0Visible && _cycle != 256 && _flags.BackgroundEnabled && !_statusFlags.Sprite0Hit && _cycle > _minimumDrawSpriteStandardCycle) {
//"The hit condition is basically sprite zero is in range AND the first sprite output unit is outputting a non-zero pixel AND the background drawing unit is outputting a non-zero pixel."
//"Sprite zero hits do not register at x=255" (cycle 256)
//"... provided that background and sprite rendering are both enabled"
//"Should always miss when Y >= 239"
_statusFlags.Sprite0Hit = true;
_console->DebugProcessEvent(EventType::SpriteZeroHit);
}
if(_settings->GetSpritesEnabled() && (backgroundColor == 0 || !_spriteTiles[i].BackgroundPriority)) {
//Check sprite priority
return _lastSprite->PaletteOffset + spriteColor;
}
break;
}
}
}
}
return ((offset + ((_cycle - 1) & 0x07) < 8) ? _previousTile : _currentTile).PaletteOffset + backgroundColor;
}
void PPU::DrawPixel()
{
//This is called 3.7 million times per second - needs to be as fast as possible.
if(IsRenderingEnabled() || ((_state.VideoRamAddr & 0x3F00) != 0x3F00)) {
uint32_t color = GetPixelColor();
_currentOutputBuffer[(_scanline << 8) + _cycle - 1] = _paletteRAM[color & 0x03 ? color : 0];
} else {
//"If the current VRAM address points in the range $3F00-$3FFF during forced blanking, the color indicated by this palette location will be shown on screen instead of the backdrop color."
_currentOutputBuffer[(_scanline << 8) + _cycle - 1] = _paletteRAM[_state.VideoRamAddr & 0x1F];
}
}
uint16_t PPU::GetCurrentBgColor()
{
uint16_t color;
if(IsRenderingEnabled() || (_state.VideoRamAddr & 0x3F00) != 0x3F00) {
color = _paletteRAM[0];
} else {
color = _paletteRAM[_state.VideoRamAddr & 0x1F];
}
return (color & _paletteRamMask) | _intensifyColorBits;
}
void PPU::UpdateGrayscaleAndIntensifyBits()
{
if(_scanline < 0 || _scanline > _nmiScanline) {
return;
}
int pixelNumber;
if(_scanline >= 240) {
pixelNumber = 61439;
} else if(_cycle < 3) {
pixelNumber = (_scanline << 8) - 1;
} else if(_cycle <= 258) {
pixelNumber = (_scanline << 8) + _cycle - 3;
} else {
pixelNumber = (_scanline << 8) + 255;
}
if(_paletteRamMask == 0x3F && _intensifyColorBits == 0) {
//Nothing to do (most common case)
_lastUpdatedPixel = pixelNumber;
return;
}
if(_lastUpdatedPixel < pixelNumber) {
uint16_t *out = _currentOutputBuffer + _lastUpdatedPixel + 1;
while(_lastUpdatedPixel < pixelNumber) {
*out = (*out & _paletteRamMask) | _intensifyColorBits;
out++;
_lastUpdatedPixel++;
}
}
}
void PPU::ProcessScanline()
{
//Only called for cycle 1+
if(_cycle <= 256) {
LoadTileInfo();
if(_prevRenderingEnabled && (_cycle & 0x07) == 0) {
IncHorizontalScrolling();
if(_cycle == 256) {
IncVerticalScrolling();
}
}
if(_scanline >= 0) {
DrawPixel();
ShiftTileRegisters();
//"Secondary OAM clear and sprite evaluation do not occur on the pre-render line"
ProcessSpriteEvaluation();
} else if(_cycle < 9) {
//Pre-render scanline logic
if(_cycle == 1) {
_statusFlags.VerticalBlank = false;
_console->GetCpu()->ClearNmiFlag();
}
if(_state.SpriteRamAddr >= 0x08 && IsRenderingEnabled() && !_settings->CheckFlag(EmulationFlags::DisableOamAddrBug)) {
//This should only be done if rendering is enabled (otherwise oam_stress test fails immediately)
//"If OAMADDR is not less than eight when rendering starts, the eight bytes starting at OAMADDR & 0xF8 are copied to the first eight bytes of OAM"
WriteSpriteRam(_cycle - 1, ReadSpriteRam((_state.SpriteRamAddr & 0xF8) + _cycle - 1));
}
}
} else if(_cycle >= 257 && _cycle <= 320) {
if(_cycle == 257) {
_spriteIndex = 0;
memset(_hasSprite, 0, sizeof(_hasSprite));
if(_prevRenderingEnabled) {
//copy horizontal scrolling value from t
_state.VideoRamAddr = (_state.VideoRamAddr & ~0x041F) | (_state.TmpVideoRamAddr & 0x041F);
_console->DebugSetLastFramePpuScroll(_state.VideoRamAddr, _state.XScroll, true);
}
}
if(IsRenderingEnabled()) {
//"OAMADDR is set to 0 during each of ticks 257-320 (the sprite tile loading interval) of the pre-render and visible scanlines." (When rendering)
_state.SpriteRamAddr = 0;
if((_cycle - 261) % 8 == 0) {
//Cycle 260, 268, etc. This is an approximation (each tile is actually loaded in 8 steps (e.g from 257 to 264))
LoadSpriteTileInfo();
} else if((_cycle - 257) % 8 == 0) {
//Garbage NT sprite fetch (257, 265, 273, etc.) - Required for proper MC-ACC IRQs (MMC3 clone)
ReadVram(GetNameTableAddr());
} else if((_cycle - 259) % 8 == 0) {
//Garbage AT sprite fetch
ReadVram(GetAttributeAddr());
}
if(_scanline == -1 && _cycle >= 280 && _cycle <= 304) {
//copy vertical scrolling value from t
_state.VideoRamAddr = (_state.VideoRamAddr & ~0x7BE0) | (_state.TmpVideoRamAddr & 0x7BE0);
}
}
} else if(_cycle >= 321 && _cycle <= 336) {
if(_cycle == 321) {
if(IsRenderingEnabled()) {
LoadExtraSprites();
_oamCopybuffer = _secondarySpriteRAM[0];
}
LoadTileInfo();
if(_scanline == -1) {
_console->DebugSetLastFramePpuScroll(_state.VideoRamAddr, _state.XScroll, false);
}
} else if(_prevRenderingEnabled && (_cycle == 328 || _cycle == 336)) {
LoadTileInfo();
_state.LowBitShift <<= 8;
_state.HighBitShift <<= 8;
IncHorizontalScrolling();
} else {
LoadTileInfo();
}
} else if(_cycle == 337 || _cycle == 339) {
if(IsRenderingEnabled()) {
ReadVram(GetNameTableAddr());
if(_scanline == -1 && _cycle == 339 && (_frameCount & 0x01) && _nesModel == NesModel::NTSC && _settings->GetPpuModel() == PpuModel::Ppu2C02) {
//This behavior is NTSC-specific - PAL frames are always the same number of cycles
//"With rendering enabled, each odd PPU frame is one PPU clock shorter than normal" (skip from 339 to 0, going over 340)
_cycle = 340;
}
}
}
}
void PPU::ProcessSpriteEvaluation()
{
if(IsRenderingEnabled() || (_nesModel == NesModel::PAL && _scanline >= _palSpriteEvalScanline)) {
if(_cycle < 65) {
//Clear secondary OAM at between cycle 1 and 64
_oamCopybuffer = 0xFF;
_secondarySpriteRAM[(_cycle - 1) >> 1] = 0xFF;
} else {
if(_cycle == 65) {
_sprite0Added = false;
_spriteInRange = false;
_secondaryOAMAddr = 0;
_overflowBugCounter = 0;
_oamCopyDone = false;
_spriteAddrH = (_state.SpriteRamAddr >> 2) & 0x3F;
_spriteAddrL = _state.SpriteRamAddr & 0x03;
_firstVisibleSpriteAddr = _spriteAddrH * 4;
_lastVisibleSpriteAddr = _firstVisibleSpriteAddr;
} else if(_cycle == 256) {
_sprite0Visible = _sprite0Added;
_spriteCount = (_secondaryOAMAddr >> 2);
}
if(_cycle & 0x01) {
//Read a byte from the primary OAM on odd cycles
_oamCopybuffer = ReadSpriteRam(_state.SpriteRamAddr);
_oamID = _state.SpriteRamAddr >> 2;
} else {
if(_oamCopyDone) {
_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."
_oamCopybuffer = _secondarySpriteRAM[_secondaryOAMAddr & 0x1F];
_oamID = _secondarySpriteRAMLink[(_secondaryOAMAddr >> 2) & 0x08];
}
} else {
if(!_spriteInRange && _scanline >= _oamCopybuffer && _scanline < _oamCopybuffer + (_flags.LargeSprites ? 16 : 8)) {
_spriteInRange = true;
}
if(_secondaryOAMAddr < 0x20) {
//Copy 1 byte to secondary OAM
_secondarySpriteRAM[_secondaryOAMAddr] = _oamCopybuffer;
_secondarySpriteRAMLink[_secondaryOAMAddr >> 2] =_oamID;
if(_spriteInRange) {
_spriteAddrL++;
_secondaryOAMAddr++;
if(_spriteAddrH == 0) {
_sprite0Added = true;
}
//Note: Using "(_secondaryOAMAddr & 0x03) == 0" instead of "_spriteAddrL == 0" is required
//to replicate a hardware bug noticed in oam_flicker_test_reenable when disabling & re-enabling
//rendering on a single scanline
if((_secondaryOAMAddr & 0x03) == 0) {
//Done copying all 4 bytes
_spriteInRange = false;
_spriteAddrL = 0;
_lastVisibleSpriteAddr = _spriteAddrH * 4;
_spriteAddrH = (_spriteAddrH + 1) & 0x3F;
if(_spriteAddrH == 0) {
_oamCopyDone = true;
}
}
} else {
//Nothing to copy, skip to next sprite
_spriteAddrH = (_spriteAddrH + 1) & 0x3F;
if(_spriteAddrH == 0) {
_oamCopyDone = true;
}
}
} else {
//"As seen above, a side effect of the OAM write disable signal is to turn writes to the secondary OAM into reads from it."
_oamCopybuffer = _secondarySpriteRAM[_secondaryOAMAddr & 0x1F];
_oamID = _secondarySpriteRAMLink[(_secondaryOAMAddr >> 2) & 0x08];
//8 sprites have been found, check next sprite for overflow + emulate PPU bug
if(_spriteInRange) {
//Sprite is visible, consider this to be an overflow
_statusFlags.SpriteOverflow = true;
_spriteAddrL = (_spriteAddrL + 1);
if(_spriteAddrL == 4) {
_spriteAddrH = (_spriteAddrH + 1) & 0x3F;
_spriteAddrL = 0;
}
if(_overflowBugCounter == 0) {
_overflowBugCounter = 3;
} else if(_overflowBugCounter > 0) {
_overflowBugCounter--;
if(_overflowBugCounter == 0) {
//"After it finishes "fetching" this sprite(and setting the overflow flag), it realigns back at the beginning of this line and then continues here on the next sprite"
_oamCopyDone = true;
_spriteAddrL = 0;
}
}
} else {
//Sprite isn't on this scanline, trigger sprite evaluation bug - increment both H & L at the same time
_spriteAddrH = (_spriteAddrH + 1) & 0x3F;
_spriteAddrL = (_spriteAddrL + 1) & 0x03;
if(_spriteAddrH == 0) {
_oamCopyDone = true;
}
}
}
}
_state.SpriteRamAddr = (_spriteAddrL & 0x03) | (_spriteAddrH << 2);
}
}
}
}
uint8_t PPU::ReadSpriteRam(uint8_t addr)
{
if(!_enableOamDecay) {
return _spriteRAM[addr];
} else {
uint64_t elapsedCycles = _console->GetCpu()->GetCycleCount() - _oamDecayCycles[addr >> 3];
if(elapsedCycles <= PPU::OamDecayCycleCount) {
_oamDecayCycles[addr >> 3] = _console->GetCpu()->GetCycleCount();
return _spriteRAM[addr];
} else {
if(_flags.SpritesEnabled) {
shared_ptr<Debugger> debugger = _console->GetDebugger(false);
if(debugger && debugger->CheckFlag(DebuggerFlags::BreakOnDecayedOamRead)) {
//When debugging with the break on decayed oam read flag turned on, break (only if sprite rendering is enabled to avoid false positives)
debugger->BreakImmediately(BreakSource::BreakOnDecayedOamRead);
}
}
//If this 8-byte row hasn't been read/written to in over 3000 cpu cycles (~1.7ms), return 0x10 to simulate decay
return 0x10;
}
}
}
void PPU::WriteSpriteRam(uint8_t addr, uint8_t value)
{
_spriteRAM[addr] = value;
if(_enableOamDecay) {
_oamDecayCycles[addr >> 3] = _console->GetCpu()->GetCycleCount();
}
}
void PPU::DebugSendFrame()
{
_console->GetVideoDecoder()->UpdateFrame(_currentOutputBuffer);
}
uint16_t* PPU::GetScreenBuffer(bool previousBuffer)
{
return previousBuffer ? ((_currentOutputBuffer == _outputBuffers[0]) ? _outputBuffers[1] : _outputBuffers[0]) : _currentOutputBuffer;
}
void PPU::DebugCopyOutputBuffer(uint16_t *target)
{
memcpy(target, _currentOutputBuffer, PPU::PixelCount * sizeof(uint16_t));
}
void PPU::SendFrame()
{
UpdateGrayscaleAndIntensifyBits();
_console->GetNotificationManager()->SendNotification(ConsoleNotificationType::PpuFrameDone, _currentOutputBuffer);
#ifdef LIBRETRO
_console->GetVideoDecoder()->UpdateFrameSync(_currentOutputBuffer);
#else
if(_console->GetRewindManager()->IsRewinding()) {
if(!_console->GetRewindManager()->IsStepBack()) {
_console->GetVideoDecoder()->UpdateFrameSync(_currentOutputBuffer);
}
} else {
//If VideoDecoder isn't done with the previous frame, UpdateFrame will block until it is ready to accept a new frame.
_console->GetVideoDecoder()->UpdateFrame(_currentOutputBuffer);
}
_enableOamDecay = _settings->CheckFlag(EmulationFlags::EnableOamDecay);
#endif
}
void PPU::BeginVBlank()
{
TriggerNmi();
}
void PPU::TriggerNmi()
{
if(_flags.VBlank) {
_console->GetCpu()->SetNmiFlag();
}
}
void PPU::UpdateApuStatus()
{
APU* apu = _console->GetApu();
apu->SetApuStatus(true);
if(_scanline > 240) {
if(_scanline > _standardVblankEnd) {
//Disable APU for extra lines after NMI
apu->SetApuStatus(false);
} else if(_scanline >= _standardNmiScanline && _scanline < _nmiScanline) {
//Disable APU for extra lines before NMI
apu->SetApuStatus(false);
}
}
}
void PPU::DebugUpdateFrameBuffer(bool toGrayscale)
{
//Clear output buffer for "Draw partial frame" feature
if(toGrayscale) {
for(int i = 0; i < PPU::PixelCount; i++) {
_currentOutputBuffer[i] &= 0x30;
}
} else {
memset(_currentOutputBuffer, 0, PPU::PixelCount * 2);
}
}
void PPU::SetOamCorruptionFlags()
{
if(!_settings->CheckFlag(EmulationFlags::EnablePpuOamRowCorruption)) {
return;
}
//Note: Still pending more research, but this currently matches a portion of the issues that have been observed
//When rendering is disabled in some sections of the screen, either:
// A- During Secondary OAM clear (first ~64 cycles)
// B- During OAM tile fetching (cycle ~256 to cycle ~320)
//then OAM memory gets corrupted the next time the PPU starts rendering again (usually at the start of the next frame)
//This usually causes the first "row" of OAM (the first 8 bytes) to get copied over another, causing some sprites to be missing
//and causing an extra set of the first 2 sprites to appear on the screen (not possible to see them except via any overflow they may cause)
if(_cycle >= 0 && _cycle < 64) {
//Every 2 dots causes the corruption to shift down 1 OAM row (8 bytes)
_corruptOamRow[_cycle >> 1] = true;
} else if(_cycle >= 256 && _cycle < 320) {
//This section is in 8-dot segments.
//The first 3 dot increment the corrupted row by 1, and then the last 5 dots corrupt the next row for 5 dots.
uint8_t base = (_cycle - 256) >> 3;
uint8_t offset = std::min<uint8_t>(3, (_cycle - 256) & 0x07);
_corruptOamRow[base * 4 + offset] = true;
}
}
void PPU::ProcessOamCorruption()
{
if(!_settings->CheckFlag(EmulationFlags::EnablePpuOamRowCorruption)) {
return;
}
//Copy first OAM row over another row, as needed by corruption flags (can be over itself, which causes no actual harm)
for(int i = 0; i < 32; i++) {
if(_corruptOamRow[i]) {
if(i > 0) {
memcpy(_spriteRAM + i * 8, _spriteRAM, 8);
}
_corruptOamRow[i] = false;
}
}
}
void PPU::Exec()
{
if(_cycle > 339) {
_cycle = 0;
if(++_scanline > _vblankEnd) {
_lastUpdatedPixel = -1;
_scanline = -1;
//Force prerender scanline sprite fetches to load the dummy $FF tiles (fixes shaking in Ninja Gaiden 3 stage 1 after beating boss)
_spriteCount = 0;
if(_renderingEnabled) {
ProcessOamCorruption();
}
UpdateMinimumDrawCycles();
}
_console->DebugProcessPpuCycle();
UpdateApuStatus();
if(_scanline == _settings->GetInputPollScanline()) {
_console->GetControlManager()->UpdateInputState();
}
//Cycle = 0
if(_scanline == -1) {
_statusFlags.SpriteOverflow = false;
_statusFlags.Sprite0Hit = false;
//Switch to alternate output buffer (VideoDecoder may still be decoding the last frame buffer)
_currentOutputBuffer = (_currentOutputBuffer == _outputBuffers[0]) ? _outputBuffers[1] : _outputBuffers[0];
} else if(_scanline == 240) {
//At the start of vblank, the bus address is set back to VideoRamAddr.
//According to Visual NES, this occurs on scanline 240, cycle 1, but is done here on cycle for performance reasons
SetBusAddress(_state.VideoRamAddr);
SendFrame();
_frameCount++;
}
} else {
//Cycle > 0
_cycle++;
_console->DebugProcessPpuCycle();
if(_scanline < 240) {
ProcessScanline();
} else if(_cycle == 1 && _scanline == _nmiScanline) {
if(!_preventVblFlag) {
_statusFlags.VerticalBlank = true;
BeginVBlank();
}
_preventVblFlag = false;
} else if(_nesModel == NesModel::PAL && _scanline >= _palSpriteEvalScanline) {
//"On a PAL machine, because of its extended vertical blank, the PPU begins refreshing OAM roughly 21 scanlines after NMI[2], to prevent it
//from decaying during the longer hiatus of rendering. Additionally, it will continue to refresh during the visible portion of the screen
//even if rendering is disabled. Because of this, OAM DMA must be done near the beginning of vertical blank on PAL, and everywhere else
//it is liable to conflict with the refresh. Since the refresh can't be disabled like on the NTSC hardware, OAM decay does not occur at all on the PAL NES."
if(_cycle <= 256) {
ProcessSpriteEvaluation();
} else if(_cycle >= 257 && _cycle < 320) {
_state.SpriteRamAddr = 0;
}
}
}
if(_needStateUpdate) {
UpdateState();
}
}
void PPU::UpdateState()
{
_needStateUpdate = false;
//Rendering enabled flag is apparently set with a 1 cycle delay (i.e setting it at cycle 5 will render cycle 6 like cycle 5 and then take the new settings for cycle 7)
if(_prevRenderingEnabled != _renderingEnabled) {
_prevRenderingEnabled = _renderingEnabled;
if(_scanline < 240) {
if(_prevRenderingEnabled) {
//Rendering was just enabled, perform oam corruption if any is pending
ProcessOamCorruption();
} else if(!_prevRenderingEnabled) {
//Rendering was just disabled by a write to $2001, check for oam row corruption glitch
SetOamCorruptionFlags();
//When rendering is disabled midscreen, set the vram bus back to the value of 'v'
SetBusAddress(_state.VideoRamAddr & 0x3FFF);
if(_cycle >= 65 && _cycle <= 256) {
//Disabling rendering during OAM evaluation will trigger a glitch causing the current address to be incremented by 1
//The increment can be "delayed" by 1 PPU cycle depending on whether or not rendering is disabled on an even/odd cycle
//e.g, if rendering is disabled on an even cycle, the following PPU cycle will increment the address by 5 (instead of 4)
// if rendering is disabled on an odd cycle, the increment will wait until the next odd cycle (at which point it will be incremented by 1)
//In practice, there is no way to see the difference, so we just increment by 1 at the end of the next cycle after rendering was disabled
_state.SpriteRamAddr++;
//Also corrupt H/L to replicate a bug found in oam_flicker_test_reenable when rendering is disabled around scanlines 128-136
//Reenabling the causes the OAM evaluation to restart misaligned, and ends up generating a single sprite that's offset by 1
//such that it's Y=tile index, index = attributes, attributes = x, and X = the next sprite's Y value
_spriteAddrH = (_state.SpriteRamAddr >> 2) & 0x3F;
_spriteAddrL = _state.SpriteRamAddr & 0x03;
}
}
}
}
if(_renderingEnabled != (_flags.BackgroundEnabled | _flags.SpritesEnabled)) {
_renderingEnabled = _flags.BackgroundEnabled | _flags.SpritesEnabled;
_needStateUpdate = true;
}
_console->DebugAddDebugEvent(DebugEventType::BgColorChange);
if(_updateVramAddrDelay > 0) {
_updateVramAddrDelay--;
if(_updateVramAddrDelay == 0) {
if(_settings->CheckFlag(EmulationFlags::EnablePpu2006ScrollGlitch) && _scanline < 240 && IsRenderingEnabled()) {
//When a $2006 address update lands on the Y or X increment, the written value is bugged and is ANDed with the incremented value
if(_cycle == 257) {
_state.VideoRamAddr &= _updateVramAddr;
shared_ptr<Debugger> debugger = _console->GetDebugger(false);
if(debugger && debugger->CheckFlag(DebuggerFlags::BreakOnPpu2006ScrollGlitch)) {
debugger->BreakImmediately(BreakSource::BreakOnPpu2006ScrollGlitch);
}
} else if(_cycle > 0 && (_cycle & 0x07) == 0 && (_cycle <= 256 || _cycle > 320)) {
_state.VideoRamAddr = (_updateVramAddr & ~0x41F) | (_state.VideoRamAddr & _updateVramAddr & 0x41F);
shared_ptr<Debugger> debugger = _console->GetDebugger(false);
if(debugger && debugger->CheckFlag(DebuggerFlags::BreakOnPpu2006ScrollGlitch)) {
debugger->BreakImmediately(BreakSource::BreakOnPpu2006ScrollGlitch);
}
} else {
_state.VideoRamAddr = _updateVramAddr;
}
} else {
_state.VideoRamAddr = _updateVramAddr;
}
if(!_renderingEnabled) {
_console->DebugAddDebugEvent(DebugEventType::BgColorChange);
}
//The glitches updates corrupt both V and T, so set the new value of V back into T
_state.TmpVideoRamAddr = _state.VideoRamAddr;
if(_scanline >= 240 || !IsRenderingEnabled()) {
//Only set the VRAM address on the bus if the PPU is not rendering
//More info here: https://forums.nesdev.com/viewtopic.php?p=132145#p132145
//Trigger bus address change when setting the vram address - needed by MMC3 IRQ counter
//"4) Should be clocked when A12 changes to 1 via $2006 write"
SetBusAddress(_state.VideoRamAddr & 0x3FFF);
}
} else {
_needStateUpdate = true;
}
}
if(_ignoreVramRead > 0) {
_ignoreVramRead--;
if(_ignoreVramRead > 0) {
_needStateUpdate = true;
}
}
}
uint8_t* PPU::GetSpriteRam()
{
//Used by debugger
if(_enableOamDecay) {
for(int i = 0; i < 0x100; i++) {
//Apply OAM decay to sprite RAM before letting debugger access it
if((_console->GetCpu()->GetCycleCount() - _oamDecayCycles[i >> 3]) > PPU::OamDecayCycleCount) {
_spriteRAM[i] = 0x10;
}
}
}
return _spriteRAM;
}
uint32_t PPU::GetPixelBrightness(uint8_t x, uint8_t y)
{
//Used by Zapper, gives a rough approximation of the brightness level of the specific pixel
uint16_t pixelData = _currentOutputBuffer[y << 8 | x];
uint32_t argbColor = _settings->GetRgbPalette()[pixelData & 0x3F];
return (argbColor & 0xFF) + ((argbColor >> 8) & 0xFF) + ((argbColor >> 16) & 0xFF);
}
void PPU::StreamState(bool saving)
{
ArrayInfo<uint8_t> paletteRam = { _paletteRAM, 0x20 };
ArrayInfo<uint8_t> spriteRam = { _spriteRAM, 0x100 };
ArrayInfo<uint8_t> secondarySpriteRam = { _secondarySpriteRAM, 0x20 };
ArrayInfo<int32_t> openBusDecayStamp = { _openBusDecayStamp, 8 };
bool disablePpu2004Reads = false;
bool disablePaletteRead = false;
bool disableOamAddrBug = false;
if(saving) {
disablePpu2004Reads = _settings->CheckFlag(EmulationFlags::DisablePpu2004Reads);
disablePaletteRead = _settings->CheckFlag(EmulationFlags::DisablePaletteRead);
disableOamAddrBug = _settings->CheckFlag(EmulationFlags::DisableOamAddrBug);
}
Stream(_state.Control, _state.Mask, _state.Status, _state.SpriteRamAddr, _state.VideoRamAddr, _state.XScroll, _state.TmpVideoRamAddr, _state.WriteToggle,
_state.HighBitShift, _state.LowBitShift, _flags.VerticalWrite, _flags.SpritePatternAddr, _flags.BackgroundPatternAddr, _flags.LargeSprites, _flags.VBlank,
_flags.Grayscale, _flags.BackgroundMask, _flags.SpriteMask, _flags.BackgroundEnabled, _flags.SpritesEnabled, _flags.IntensifyRed, _flags.IntensifyGreen,
_flags.IntensifyBlue, _paletteRamMask, _intensifyColorBits, _statusFlags.SpriteOverflow, _statusFlags.Sprite0Hit, _statusFlags.VerticalBlank, _scanline,
_cycle, _frameCount, _memoryReadBuffer, _currentTile.LowByte, _currentTile.HighByte, _currentTile.PaletteOffset, _nextTile.LowByte, _nextTile.HighByte,
_nextTile.PaletteOffset, _nextTile.TileAddr, _previousTile.LowByte, _previousTile.HighByte, _previousTile.PaletteOffset, _spriteIndex, _spriteCount,
_secondaryOAMAddr, _sprite0Visible, _oamCopybuffer, _spriteInRange, _sprite0Added, _spriteAddrH, _spriteAddrL, _oamCopyDone, _nesModel,
_prevRenderingEnabled, _renderingEnabled, _openBus, _ignoreVramRead, paletteRam, spriteRam, secondarySpriteRam,
openBusDecayStamp, disablePpu2004Reads, disablePaletteRead, disableOamAddrBug, _overflowBugCounter, _updateVramAddr, _updateVramAddrDelay,
_needStateUpdate, _ppuBusAddress, _preventVblFlag, _masterClock);
for(int i = 0; i < 64; i++) {
Stream(_spriteTiles[i].SpriteX, _spriteTiles[i].LowByte, _spriteTiles[i].HighByte, _spriteTiles[i].PaletteOffset, _spriteTiles[i].HorizontalMirror, _spriteTiles[i].BackgroundPriority);
}
if(!saving) {
_settings->SetFlagState(EmulationFlags::DisablePpu2004Reads, disablePpu2004Reads);
_settings->SetFlagState(EmulationFlags::DisablePaletteRead, disablePaletteRead);
_settings->SetFlagState(EmulationFlags::DisableOamAddrBug, disableOamAddrBug);
SetNesModel(_nesModel);
UpdateMinimumDrawCycles();
for(int i = 0; i < 0x20; i++) {
//Set oam decay cycle to the current cycle to ensure it doesn't decay when loading a state
_oamDecayCycles[i] = _console->GetCpu()->GetCycleCount();
}
memset(_corruptOamRow, 0, sizeof(_corruptOamRow));
for(int i = 0; i < 257; i++) {
_hasSprite[i] = true;
}
_lastUpdatedPixel = -1;
UpdateApuStatus();
}
}