#include "pch.h" #include "Utilities/Serializer.h" #include "NES/NesPpu.h" #include "NES/NesCpu.h" #include "NES/APU/NesApu.h" #include "NES/NesMemoryManager.h" #include "NES/NesConsole.h" #include "NES/NesControlManager.h" #include "NES/BaseMapper.h" #include "NES/NesConstants.h" #include "NES/NesDefaultVideoFilter.h" #include "NES/DefaultNesPpu.h" #include "NES/NsfPpu.h" #include "NES/HdPacks/HdNesPpu.h" #include "NES/HdPacks/HdBuilderPpu.h" #include "Shared/EmuSettings.h" #include "Shared/Video/VideoDecoder.h" #include "Shared/RewindManager.h" #include "Shared/NotificationManager.h" #include "Shared/RenderedFrame.h" #include "Shared/MemoryOperationType.h" #include "Shared/Utilities/AvMergeUtilities.h" #include "Shared/EventType.h" #include "Utilities/BitUtilities.h" template NesPpu::NesPpu(NesConsole* console) { _console = console; _emu = console->GetEmulator(); _mapper = console->GetMapper(); _masterClock = 0; _masterClockDivider = 4; _settings = _emu->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)); if(_emu->GetSettings()->GetNesConfig().RamPowerOnState == RamState::Random) { _console->InitializeRam(_paletteRam, 0x20); for(int i = 0; i < 0x20; i++) { _paletteRam[i] &= 0x3F; } } else { //When not using random ram, use a static state at power on (matches blargg's old palette test rom) constexpr 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)); } //'v' is not cleared on reset, but it set to 0 on power on _videoRamAddr = 0; _emu->RegisterMemory(MemoryType::NesSpriteRam, _spriteRam, sizeof(_spriteRam)); _emu->RegisterMemory(MemoryType::NesSecondarySpriteRam, _secondarySpriteRam, sizeof(_secondarySpriteRam)); _emu->RegisterMemory(MemoryType::NesPaletteRam, _paletteRam, sizeof(_paletteRam)); _console->InitializeRam(_spriteRam, 0x100); _console->InitializeRam(_secondarySpriteRam, 0x20); UpdateTimings(ConsoleRegion::Ntsc); Reset(false); } template void NesPpu::Reset(bool softReset) { _masterClock = 0; //Reset OAM decay timestamps regardless of the reset PPU option memset(_oamDecayCycles, 0, sizeof(_oamDecayCycles)); _enableOamDecay = _console->GetNesConfig().EnableOamDecay; if(softReset && _settings->GetNesConfig().DisablePpuReset) { return; } _preventVblFlag = false; _needStateUpdate = false; _prevRenderingEnabled = false; _renderingEnabled = false; _ignoreVramRead = 0; _openBus = 0; memset(_openBusDecayStamp, 0, sizeof(_openBusDecayStamp)); _tmpVideoRamAddr = 0; _highBitShift = 0; _lowBitShift = 0; _spriteRamAddr = 0; _xScroll = 0; _writeToggle = false; _control = {}; _mask = {}; if(!softReset) { //"The VBL flag (PPUSTATUS bit 7) is random at power, and unchanged by reset." _statusFlags = {}; _statusFlags.VerticalBlank = _settings->GetNesConfig().RandomizeMapperPowerOnState ? _settings->GetRandomBool() : false; } _tile = {}; _currentTilePalette = 0; _previousTilePalette = 0; _ppuBusAddress = 0; _intensifyColorBits = 0; _paletteRamMask = 0x3F; _lastUpdatedPixel = -1; _lastSprite = nullptr; _oamCopybuffer = 0; _spriteInRange = false; _sprite0Added = false; _oamCopyDone = false; memset(_spriteTiles, 0, sizeof(_spriteTiles)); _spriteCount = 0; _secondaryOamAddr = 0; _sprite0Visible = false; _spriteIndex = 0; //First execution will be cycle 0, scanline 0 _scanline = -1; _cycle = 340; _frameCount = 1; _memoryReadBuffer = 0; _overflowBugCounter = 0; _updateVramAddrDelay = 0; _updateVramAddr = 0; _firstVisibleSpriteAddr = 0; _lastVisibleSpriteAddr = 0; _allowFullPpuAccess = false; UpdateMinimumDrawCycles(); } template void NesPpu::UpdateTimings(ConsoleRegion region, bool overclockAllowed) { _region = region; switch(_region) { case ConsoleRegion::Auto: //Should never be Auto break; case ConsoleRegion::Ntsc: _nmiScanline = 241; _vblankEnd = 260; _standardNmiScanline = 241; _standardVblankEnd = 260; _masterClockDivider = 4; break; case ConsoleRegion::Pal: _nmiScanline = 241; _vblankEnd = 310; _standardNmiScanline = 241; _standardVblankEnd = 310; _masterClockDivider = 5; break; case ConsoleRegion::Dendy: _nmiScanline = 291; _vblankEnd = 310; _standardNmiScanline = 291; _standardVblankEnd = 310; _masterClockDivider = 5; break; } if(overclockAllowed) { NesConfig& cfg = _console->GetNesConfig(); _nmiScanline += cfg.PpuExtraScanlinesBeforeNmi; _standardVblankEnd += cfg.PpuExtraScanlinesBeforeNmi; _vblankEnd += cfg.PpuExtraScanlinesAfterNmi + cfg.PpuExtraScanlinesBeforeNmi; } _palSpriteEvalScanline = _nmiScanline + 24; } template void NesPpu::UpdateVideoRamAddr() { if(_scanline >= 240 || !IsRenderingEnabled()) { _videoRamAddr = (_videoRamAddr + (_control.VerticalWrite ? 32 : 1)) & 0x7FFF; //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(_videoRamAddr & 0x3FFF); if(!_renderingEnabled) { _emu->AddDebugEvent(DebugEventType::BgColorChange); } } 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(); } } template void NesPpu::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] > 3) { //Decay bits to 0 after 3 frames //This is a very conservative estimate, individual bits tend to decay //much faster than this. openBus &= 0xFF7F; } value >>= 1; mask >>= 1; } _openBus = (uint8_t)openBus; } } template uint8_t NesPpu::ApplyOpenBus(uint8_t mask, uint8_t value) { SetOpenBus(~mask, value); return value | (_openBus & mask); } template PpuModel NesPpu::GetPpuModel() { if(_mapper->GetGameSystem() == GameSystem::VsSystem) { return _mapper->GetPpuModel(); } else { return PpuModel::Ppu2C02; } } template void NesPpu::ProcessStatusRegOpenBus(uint8_t& openBusMask, uint8_t& returnValue) { switch(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; } } template uint8_t NesPpu::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(!_console->GetNesConfig().DisablePpu2004Reads) { if(_scanline <= 239 && IsRenderingEnabled()) { if((_cycle >= 257 && _cycle <= 340) || _cycle == 0) { returnValue = _secondarySpriteRam[_secondaryOamAddr & 0x1F]; } returnValue = _oamCopybuffer; } else { returnValue = _spriteRam[_spriteRamAddr]; } openBusMask = 0x00; } break; case PpuRegisters::VideoMemoryData: returnValue = _memoryReadBuffer; if((_videoRamAddr & 0x3FFF) >= 0x3F00 && !_console->GetNesConfig().DisablePaletteRead) { returnValue = (ReadPaletteRam(_videoRamAddr) & _paletteRamMask) | (_openBus & 0xC0); openBusMask = 0xC0; } else { openBusMask = 0x00; } break; default: break; } return returnValue | (_openBus & openBusMask); } template uint8_t NesPpu::ReadRam(uint16_t addr) { uint8_t openBusMask = 0xFF; uint8_t returnValue = 0; switch(GetRegisterID(addr)) { case PpuRegisters::Status: _writeToggle = false; returnValue = ((uint8_t)_statusFlags.SpriteOverflow << 5) | ((uint8_t)_statusFlags.Sprite0Hit << 6) | ((uint8_t)_statusFlags.VerticalBlank << 7); UpdateStatusFlag(); openBusMask = 0x1F; ProcessStatusRegOpenBus(openBusMask, returnValue); break; case PpuRegisters::SpriteData: if(!_console->GetNesConfig().DisablePpu2004Reads) { if(_scanline <= 239 && IsRenderingEnabled()) { //While the screen is being drawn if((_cycle >= 257 && _cycle <= 340) || _cycle == 0) { //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 _oamCopybuffer = _secondarySpriteRam[_secondaryOamAddr & 0x1F]; } //Return the value that PPU is currently using for sprite evaluation/rendering returnValue = _oamCopybuffer; } else { returnValue = ReadSpriteRam(_spriteRamAddr); _emu->ProcessPpuRead(_spriteRamAddr, returnValue, MemoryType::NesSpriteRam); } openBusMask = 0x00; } break; case PpuRegisters::VideoMemoryData: if(!_allowFullPpuAccess && _settings->GetNesConfig().RestrictPpuAccessOnFirstFrame) { openBusMask = 0x00; returnValue = 0; } else 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; // The PPU Read Buffer is not updated until the CPU read ends, and 2 more ppu cycles have passed. _ppuMemoryDataReadStateMachine = 5; if((_ppuBusAddress & 0x3FFF) >= 0x3F00 && !_console->GetNesConfig().DisablePaletteRead) { //Note: When grayscale is turned on, the read values also have the grayscale mask applied to them returnValue = (ReadPaletteRam(_ppuBusAddress) & _paletteRamMask) | (_openBus & 0xC0); _emu->ProcessPpuRead(_ppuBusAddress, returnValue, MemoryType::NesPpuMemory); openBusMask = 0xC0; } else { openBusMask = 0x00; } if(_scanline < 240 && IsRenderingEnabled()) { _emu->BreakIfDebugging(CpuType::Nes, BreakSource::NesInvalidVramAccess); } _ignoreVramRead = 6; _needStateUpdate = true; } break; default: break; } return ApplyOpenBus(openBusMask, returnValue); } template void NesPpu::WriteRam(uint16_t addr, uint8_t value) { if(addr != 0x4014) { SetOpenBus(0xFF, value); } switch(GetRegisterID(addr)) { case PpuRegisters::Control: if(GetPpuModel() >= PpuModel::Ppu2C05A && GetPpuModel() <= PpuModel::Ppu2C05E) { SetMaskRegister(value); } else { SetControlRegister(value); } break; case PpuRegisters::Mask: if(GetPpuModel() >= PpuModel::Ppu2C05A && GetPpuModel() <= PpuModel::Ppu2C05E) { SetControlRegister(value); } else { SetMaskRegister(value); } break; case PpuRegisters::SpriteAddr: //If OAM1ADDR is selected, updating the address can corrupt 2 rows (the open bus row and the destination row). //This is actually alignment-dependent, but we're currently modeling worst-case behavior. if(_settings->GetNesConfig().EnablePpuOamRowCorruption && _region == ConsoleRegion::Ntsc && (!_prevRenderingEnabled || _scanline >= 240 || (_cycle < 257 && (_cycle & 1)))) { uint8_t sourceRow = _spriteRamAddr >> 3; uint8_t openBusRow = _console->GetMemoryManager()->GetOpenBus() >> 3; uint8_t destRow = value >> 3; CorruptOamRow(sourceRow, openBusRow); CorruptOamRow(openBusRow, destRow); } //TODO There is some kind of delay on committing the $2003 value on PAL (this is how it fixes the corruption issue). //This currently isn't emulated. //"In the 2C02, writes to $2003 took effect immediately. In the 2C07, they're buffered through pclk0+pclk1+pclk0[+pclk1+pclk0]. //I'm guessing that's what it took to prevent corruption. (it's actually pclk0+pclk1+pclk0 NOR pclk0+pclk1+pclk0+pclk1+pclk0)" _spriteRamAddr = value; break; case PpuRegisters::SpriteData: //TODO: The $2003 increment can be eaten if it happens at the same time as another increment (from sprite processing or PAL's forced refresh). //The increment appears to happen 1.5-2.5 dots after the write ends. if(_scanline >= 240 || !IsRenderingEnabled()) { if((_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; } _emu->ProcessPpuWrite(_spriteRamAddr, value, MemoryType::NesSpriteRam); WriteSpriteRam(_spriteRamAddr, value); if(_region == ConsoleRegion::Ntsc || _scanline < _palSpriteEvalScanline || ((_cycle & 1) && (_cycle != 339))) { _spriteRamAddr++; } } 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" //TODO: This should actually write the contents of the OAM buffer to the current OAM1 or OAM2 byte, 1.0-2.0 dots after the write ends. _spriteRamAddr = (_spriteRamAddr + 4) & 0xFC; _emu->BreakIfDebugging(CpuType::Nes, BreakSource::NesInvalidOamWrite); } break; case PpuRegisters::ScrollOffsets: if(!_allowFullPpuAccess && _settings->GetNesConfig().RestrictPpuAccessOnFirstFrame) { return; } if(_writeToggle) { _tmpVideoRamAddr = (_tmpVideoRamAddr & ~0x73E0) | ((value & 0xF8) << 2) | ((value & 0x07) << 12); } else { _xScroll = value & 0x07; uint16_t newAddr = (_tmpVideoRamAddr & ~0x001F) | (value >> 3); ProcessTmpAddrScrollGlitch(newAddr, _console->GetMemoryManager()->GetOpenBus() >> 3, 0x001F); } _writeToggle = !_writeToggle; break; case PpuRegisters::VideoMemoryAddr: if(!_allowFullPpuAccess && _settings->GetNesConfig().RestrictPpuAccessOnFirstFrame) { return; } if(_writeToggle) { _tmpVideoRamAddr = (_tmpVideoRamAddr & ~0x00FF) | value; //Video RAM update is apparently delayed by 3 PPU cycles (based on Visual NES findings) _needStateUpdate = true; _updateVramAddrDelay = 3; _updateVramAddr = _tmpVideoRamAddr; } else { uint16_t newAddr = (_tmpVideoRamAddr & ~0xFF00) | ((value & 0x3F) << 8); ProcessTmpAddrScrollGlitch(newAddr, _console->GetMemoryManager()->GetOpenBus() << 8, 0x0C00); } _writeToggle = !_writeToggle; break; case PpuRegisters::VideoMemoryData: // The write to VRAM does not occur until the CPU write ends, and 2 more ppu cycles have passed. _ppuMemoryDataWriteStateMachine = 5; _ppuMemoryDataWriteLatch = value; _needStateUpdate = true; break; case PpuRegisters::SpriteDMA: _console->GetCpu()->RunDMATransfer(value); break; default: break; } } template void NesPpu::ProcessTmpAddrScrollGlitch(uint16_t normalAddr, uint16_t value, uint16_t mask) { _tmpVideoRamAddr = normalAddr; if(_cycle == 257 && _console->GetNesConfig().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) _videoRamAddr = (_videoRamAddr & ~mask) | (value & mask); _emu->BreakIfDebugging(CpuType::Nes, BreakSource::NesBreakOnPpuScrollGlitch); } } template void NesPpu::SetControlRegister(uint8_t value) { if(!_allowFullPpuAccess && _settings->GetNesConfig().RestrictPpuAccessOnFirstFrame) { return; } uint8_t nameTable = (value & 0x03); uint16_t normalAddr = (_tmpVideoRamAddr & ~0x0C00) | (nameTable << 10); ProcessTmpAddrScrollGlitch(normalAddr, _console->GetMemoryManager()->GetOpenBus() << 10, 0x0400); _control.VerticalWrite = (value & 0x04) == 0x04; _control.SpritePatternAddr = ((value & 0x08) == 0x08) ? 0x1000 : 0x0000; _control.BackgroundPatternAddr = ((value & 0x10) == 0x10) ? 0x1000 : 0x0000; _control.LargeSprites = (value & 0x20) == 0x20; _control.SecondaryPpu = (value & 0x40) == 0x40; if(_control.SecondaryPpu) { _emu->BreakIfDebugging(CpuType::Nes, BreakSource::NesBreakOnExtOutputMode); } _control.NmiOnVerticalBlank = (value & 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(!_control.NmiOnVerticalBlank) { _console->GetCpu()->ClearNmiFlag(); } else if(_control.NmiOnVerticalBlank && _statusFlags.VerticalBlank) { _console->GetCpu()->SetNmiFlag(); } } template void NesPpu::SetMaskRegister(uint8_t value) { if(!_allowFullPpuAccess && _settings->GetNesConfig().RestrictPpuAccessOnFirstFrame) { return; } _mask.Grayscale = (value & 0x01) == 0x01; _mask.BackgroundMask = (value & 0x02) == 0x02; _mask.SpriteMask = (value & 0x04) == 0x04; _mask.BackgroundEnabled = (value & 0x08) == 0x08; _mask.SpritesEnabled = (value & 0x10) == 0x10; _mask.IntensifyBlue = (value & 0x80) == 0x80; if(_region == ConsoleRegion::Ntsc) { _mask.IntensifyRed = (value & 0x20) == 0x20; _mask.IntensifyGreen = (value & 0x40) == 0x40; } else if(_region == ConsoleRegion::Pal || _region == ConsoleRegion::Dendy) { //"Note that on the Dendy and PAL NES, the green and red bits swap meaning." _mask.IntensifyRed = (value & 0x40) == 0x40; _mask.IntensifyGreen = (value & 0x20) == 0x20; } if(_renderingEnabled != (_mask.BackgroundEnabled | _mask.SpritesEnabled)) { _needStateUpdate = true; } UpdateMinimumDrawCycles(); UpdateGrayscaleAndIntensifyBits(); _emu->AddDebugEvent(DebugEventType::BgColorChange); } template void NesPpu::UpdateStatusFlag() { _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 template void NesPpu::IncVerticalScrolling() { uint16_t addr = _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 } _videoRamAddr = addr; } //Taken from http://wiki.nesdev.com/w/index.php/The_skinny_on_NES_scrolling#Wrapping_around template void NesPpu::IncHorizontalScrolling() { //Increase coarse X scrolling value. uint16_t addr = _videoRamAddr; if((addr & 0x001F) == 31) { //When the value is 31, wrap around to 0 and switch nametable addr = (addr & ~0x001F) ^ 0x0400; } else { addr++; } _videoRamAddr = addr; } //Taken from http://wiki.nesdev.com/w/index.php/The_skinny_on_NES_scrolling#Tile_and_attribute_fetching template uint16_t NesPpu::GetNameTableAddr() { return 0x2000 | (_videoRamAddr & 0x0FFF); } //Taken from http://wiki.nesdev.com/w/index.php/The_skinny_on_NES_scrolling#Tile_and_attribute_fetching template uint16_t NesPpu::GetAttributeAddr() { return 0x23C0 | (_videoRamAddr & 0x0C00) | ((_videoRamAddr >> 4) & 0x38) | ((_videoRamAddr >> 2) & 0x07); } template void NesPpu::SetBusAddress(uint16_t addr) { _ppuBusAddress = addr; if(_mapper->HasVramAddressHook()) { _mapper->NotifyVramAddressChange(addr); } } template uint8_t NesPpu::ReadVram(uint16_t addr, MemoryOperationType type) { SetBusAddress(addr); return _mapper->ReadVram(addr, type); } template void NesPpu::WriteVram(uint16_t addr, uint8_t value) { SetBusAddress(addr); _mapper->WriteVram(addr, value); } template void NesPpu::LoadTileInfo() { switch(_cycle & 0x07) { case 0: //This is the last dot of the 8-dot sequence (e.g dot 8, 16, etc.) //Replace bottom 8 bits of the BG shifters _highBitShift &= 0xFF00; _highBitShift |= _tile.HighByte; _lowBitShift &= 0xFF00; _lowBitShift |= _tile.LowByte; _previousTilePalette = _currentTilePalette; _currentTilePalette = _tile.PaletteOffset; ((T*)this)->PushTileInformation(); //Used by HD packs break; case 1: { uint8_t tileIndex = ReadVram(GetNameTableAddr()); _tile.TileAddr = (tileIndex << 4) | (_videoRamAddr >> 12) | _control.BackgroundPatternAddr; ((T*)this)->StoreTileInformation(); //Used by HD packs break; } case 3: { uint8_t shift = ((_videoRamAddr >> 4) & 0x04) | (_videoRamAddr & 0x02); _tile.PaletteOffset = ((ReadVram(GetAttributeAddr()) >> shift) & 0x03) << 2; break; } case 5: _tile.LowByte = ReadVram(_tile.TileAddr); break; case 7: _tile.HighByte = ReadVram(_tile.TileAddr + 8); break; } } template void NesPpu::LoadSprite(uint8_t spriteY, uint8_t tileIndex, uint8_t attributes, uint8_t spriteX, bool extraSprite) { bool backgroundPriority = (attributes & 0x20) == 0x20; bool horizontalMirror = (attributes & 0x40) == 0x40; bool verticalMirror = (attributes & 0x80) == 0x80; uint16_t tileAddr; const uint8_t spriteSizeMask = _control.LargeSprites ? 15 : 7; //This function is only called on rendering scanlines (0-239) or pre-render (-1). We need to handle the pre-render scanline numbers manually. //These are deliberately truncated to 8 bits to match hardware behavior. const uint8_t scanline8Bit = (uint8_t)(_scanline >= 0 ? _scanline : (_region == ConsoleRegion::Ntsc ? 261 : 311)); uint16_t rangeResult = scanline8Bit - spriteY; if(verticalMirror) { rangeResult ^= spriteSizeMask; } if(_control.LargeSprites) { tileAddr = (((tileIndex & 0x01) << 12) | ((tileIndex & ~0x01) << 4)) + ((rangeResult & 0x08) << 1) + (rangeResult & 0x07); } else { tileAddr = (_control.SpritePatternAddr | (tileIndex << 4)) + (rangeResult & 0x07); } NesSpriteInfo& info = _spriteTiles[_spriteIndex]; info.BackgroundPriority = backgroundPriority; info.PaletteOffset = ((attributes & 0x03) << 2) | 0x10; if(extraSprite) { //Use DebugReadVram for extra sprites to prevent side-effects. info.LowByte = _mapper->DebugReadVram(tileAddr); info.HighByte = _mapper->DebugReadVram(tileAddr + 8); } else { info.LowByte = ReadVram(tileAddr); info.HighByte = ReadVram(tileAddr + 8); } info.SpriteX = spriteX; bool inRange = rangeResult <= spriteSizeMask; if(inRange) { if(horizontalMirror) { info.LowByte = BitUtilities::ReverseByte(info.LowByte); info.HighByte = BitUtilities::ReverseByte(info.HighByte); } ((T*)this)->StoreSpriteInformation(horizontalMirror, verticalMirror, tileAddr, rangeResult, info); //Used by HD packs if(!extraSprite) { _spriteShifterList[_spriteIndex] = (((uint16_t)spriteX + 1) << 4) | _spriteIndex; //Clear expired flag to indicate that the counter was reloaded _expiredSpriteShifters &= ~(1 << _spriteIndex); } _spriteCount++; _processSprites = true; } else if(!extraSprite) { info.LowByte = 0; info.HighByte = 0; _spriteShifterList[_spriteIndex] = BaseNesPpu::SpriteShifterDone; } _spriteIndex++; } template void NesPpu::LoadExtraSprites() { if(_spriteCount == 8 && ((T*)this)->RemoveSpriteLimit() && _scanline >= 0) { bool loadExtraSprites = true; if(((T*)this)->UseAdaptiveSpriteLimit()) { 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 + (_control.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) & 0xFC; i != (uint32_t)(_firstVisibleSpriteAddr & 0xFC); i = (i + 4) & 0xFC) { uint8_t spriteY = _spriteRam[i]; if(_scanline >= spriteY && _scanline < spriteY + (_control.LargeSprites ? 16 : 8)) { LoadSprite(spriteY, _spriteRam[i + 1], _spriteRam[i + 2], _spriteRam[i + 3], true); } } } } } template void NesPpu::LoadSpriteTileInfo() { //Set it to whatever shifter we're filling. _spriteIndex = (_cycle - 257) >> 3; uint8_t* spriteAddr = _secondarySpriteRam + _spriteIndex * 4; LoadSprite(*spriteAddr, *(spriteAddr + 1), *(spriteAddr + 2), *(spriteAddr + 3), false); } template void NesPpu::ShiftTileRegisters() { _lowBitShift <<= 1; _highBitShift <<= 1; //The high bit shifter is filled with 1s as it shifts (the low bit shifter is not) _highBitShift |= 1; } template uint8_t NesPpu::GetPixelColor() { uint8_t offset = _xScroll; uint8_t backgroundColor = 0; uint8_t spriteBgColor = 0; if(_cycle > _minimumDrawBgCycle) { //BackgroundMask = false: Hide background in leftmost 8 pixels of screen spriteBgColor = (((_lowBitShift << offset) & 0x8000) >> 15) | (((_highBitShift << offset) & 0x8000) >> 14); if(_emulatorBgEnabled) { backgroundColor = spriteBgColor; } } int8_t spriteIndex = -1; uint8_t spriteColor = 0; if(_processSprites && _prevRenderingEnabled) { //If the dot is skipped, all sprite shifters are active on the first dot of the scanline. uint8_t remainingShifters = _dotSkipped ? 0xFF : _activeSpriteShifters; _lastSprite = &_spriteTiles[BitUtilities::GetHighestBitIndex(_activeSpriteShifters)]; //Output+shift all active sprite shifters. while(remainingShifters) { uint8_t i = BitUtilities::GetHighestBitIndex(remainingShifters); remainingShifters &= ~(1 << i); NesSpriteInfo& sprite = _spriteTiles[i]; //Get the color. We start with the lowest priority shifter first so the higher priority ones override it. uint8_t currColor = ((sprite.HighByte >> 6) & 0x2) | (sprite.LowByte >> 7); if(currColor != 0) { spriteIndex = i; spriteColor = currColor; _lastSprite = &sprite; } sprite.HighByte <<= 1; sprite.LowByte <<= 1; //If the shifter is empty, deactivate it. if(!(sprite.HighByte | sprite.LowByte)) { _activeSpriteShifters &= ~(1 << i); UpdateProcessSpritesFlag(); } } if(_cycle > _minimumDrawSpriteCycle && _mask.SpritesEnabled) { if(_spriteCount > 8 && spriteColor == 0) { for(spriteIndex = 8; spriteIndex < _spriteCount; spriteIndex++) { NesSpriteInfo& sprite = _spriteTiles[spriteIndex]; uint32_t shift = (int32_t)_cycle - sprite.SpriteX - 1; if(shift < 8) { _lastSprite = &sprite; spriteColor = ((sprite.LowByte << shift) & 0x80) >> 7 | ((sprite.HighByte << shift) & 0x80) >> 6; if(spriteColor) { break; } } } } if(spriteColor != 0) { if(_sprite0Visible && spriteIndex == 0 && spriteColor != 0 && spriteBgColor != 0 && _cycle != 256 && _mask.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; _emu->AddDebugEvent(DebugEventType::SpriteZeroHit); } if(_emulatorSpritesEnabled && (backgroundColor == 0 || !_spriteTiles[spriteIndex].BackgroundPriority)) { //Check sprite priority return _spriteTiles[spriteIndex].PaletteOffset + spriteColor; } } } } return ((offset + ((_cycle - 1) & 0x07) < 8) ? _previousTilePalette : _currentTilePalette) + backgroundColor; } template void NesPpu::ProcessRenderingDisabledPixel() { //Duplicated logic from ProcessScanlineImpl, only called when rendering is disabled if(_scanline >= 0) { ProcessSpriteShifters(); ((T*)this)->DrawPixel(); } else if(_cycle == 1) { _statusFlags.VerticalBlank = false; _console->GetCpu()->ClearNmiFlag(); } } template void NesPpu::ProcessScanlineImpl() { //Only called for cycle 1+ if(_cycle <= 256) { if(_prevRenderingEnabled) { //Process visible pixels when rendering is enabled //Portions are duplicated in ProcessRenderingDisabledPixel for performance reasons if(_scanline >= 0) { //"Secondary OAM clear and sprite evaluation do not occur on the pre-render line" ProcessSpriteShifters(); ((T*)this)->DrawPixel(); ProcessSpriteEvaluation(); ShiftTileRegisters(); } else if(_cycle == 1) { //Pre-render scanline logic _statusFlags.VerticalBlank = false; _console->GetCpu()->ClearNmiFlag(); } if((_cycle & 0x07) == 0) { IncHorizontalScrolling(); if(_cycle == 256) { IncVerticalScrolling(); } } LoadTileInfo(); } else { ProcessRenderingDisabledPixel(); } } else if(_cycle >= 257 && _cycle <= 320) { if(_prevRenderingEnabled) { //sprite_0_on_next_scanline is copied to sprite_0_on_this_scanline every dot in this range. _sprite0Visible = _sprite0Added; //"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) _spriteRamAddr = 0; switch((_cycle - 257) % 8) { //Garbage NT sprite fetch (257, 265, 273, etc.) - Required for proper MC-ACC IRQs (MMC3 clone) case 0: ReadVram(GetNameTableAddr()); break; //Garbage NT sprite fetch case 2: ReadVram(GetNameTableAddr()); break; //Cycle 260, 268, etc. This is an approximation (each tile is actually loaded in 8 steps (e.g from 257 to 264)) case 4: LoadSpriteTileInfo(); break; } if(_scanline == -1 && _cycle >= 280 && _cycle <= 304) { //copy vertical scrolling value from t _videoRamAddr = (_videoRamAddr & ~0x7BE0) | (_tmpVideoRamAddr & 0x7BE0); } // Load the extra sprites before tile loading starts (which matters for MMC5) // The CHR banks are switched back to the bg tileset in LoadTileInfo on cycle 321 // and the extra sprites will potentially read from the wrong banks if(_cycle == 320) { LoadExtraSprites(); } //Increment OAM2ADDR during the first 4 dots of each set of 8. There is special behavior where the first set of 8 skips the //first increment (because OAM2ADDR is being cleared), and an extra increment is done after the last set of 8. _secondaryOamAddr += !((_cycle - 1) & 4); } if(_cycle == 257) { _spriteIndex = 0; _spriteCount = 0; if(_prevRenderingEnabled) { //copy horizontal scrolling value from t _videoRamAddr = (_videoRamAddr & ~0x041F) | (_tmpVideoRamAddr & 0x041F); //This is reset during dots 256.5-257.0, overriding the increment that would happen in 257. _secondaryOamAddr = 0; } } } else if(_cycle >= 321 && _cycle <= 336) { if(_prevRenderingEnabled) { if(_cycle == 321) { //Do the final increment, which follows the last set of 8 sprite fetch dots. _secondaryOamAddr++; } else if(_prevRenderingEnabled && (_cycle == 328 || _cycle == 336)) { IncHorizontalScrolling(); } ShiftTileRegisters(); LoadTileInfo(); } } else if(_cycle == 337) { if(IsRenderingEnabled()) { _tile.TileAddr = ReadVram(GetNameTableAddr()); } } else if(_cycle == 339) { _activeSpriteShifters = 0; if(IsRenderingEnabled()) { _tile.TileAddr = ReadVram(GetNameTableAddr()); //Set sprite shifters to "count" mode (only if rendering is enabled) //Only do this if the shifter did not finish counting already (which can happen in //some scenarios when rendering is disabled mid-scanline, etc.) for(int i = 0; i < 8; i++) { uint8_t bit = (1 << (_spriteShifterList[i] & 0x07)); if(_spriteShifterList[i] != NesPpu::SpriteShifterDone && !(_expiredSpriteShifters & bit)) { _countingSpriteShifters |= bit; } } if(_scanline == -1 && _cycle == 339 && (_frameCount & 0x01) && _region == ConsoleRegion::Ntsc && 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; //Set a counter for this so it'll be nonzero for GetPixelColor on dot 1, forcing sprite shifter output. _dotSkipped = 3; _needStateUpdate = true; // Delay all sprites by 1 pixel. for(int i = 0; i < 8; i++) { _spriteShifterList[i] += 1 << 4; } _nextSpriteShifterCycle++; } } for(int i = 0; i < 8; i++) { //Any shifter that's not in count mode should be active at the start of the //next scanline and output pixels starting from X=0 (if rendering is enabled) if(_spriteShifterList[i] != NesPpu::SpriteShifterDone) { uint8_t bit = (1 << (_spriteShifterList[i] & 0x07)); if(!(_countingSpriteShifters & bit)) { _activeSpriteShifters |= bit; } } } UpdateProcessSpritesFlag(); } } template void NesPpu::UpdateProcessSpritesFlag() { _processSprites = _spriteCount || _activeSpriteShifters || _dotSkipped; } 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. _firstVisibleSpriteAddr = _spriteRamAddr & 0xFC; _lastVisibleSpriteAddr = _firstVisibleSpriteAddr; } template void NesPpu::ProcessSpriteShifters() { //Handle sprite shifter counting. if(_nextSpriteShifterCycle == _cycle) { while((uint32_t)(_spriteShifterList[_nextSpriteShifter] >> 4) == _cycle) { uint8_t bit = (1 << (_spriteShifterList[_nextSpriteShifter] & 7)); if(_countingSpriteShifters & bit) { //If shifter was in count mode, turn off count mode, //set the expired flag (counting is done) and enable "shift" mode _activeSpriteShifters |= bit; _expiredSpriteShifters |= bit; _countingSpriteShifters &= ~bit; } _nextSpriteShifter++; } _nextSpriteShifterCycle = _spriteShifterList[_nextSpriteShifter] >> 4; } } template void NesPpu::ProcessSpriteEvaluation() { if(_cycle < 65) { //Clear secondary OAM at between cycle 1 and 64 _oamCopybuffer = 0xFF; _secondarySpriteRam[_secondaryOamAddr & 0x1F] = 0xFF; _secondaryOamAddr += !(_cycle & 1); } else { if(_cycle & 0x01) { if(_cycle == 65) { ProcessSpriteEvaluationStart(); } //Read a byte from the primary OAM on odd cycles _oamCopybuffer = ReadSpriteRam(_spriteRamAddr); } else { uint8_t spriteAddrH = _spriteRamAddr >> 2; uint8_t spriteAddrL = _spriteRamAddr & 3; //(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. if(_oamCopyDone && !_settings->GetNesConfig().EnablePpuSpriteEvalBug) { spriteAddrH = (spriteAddrH + 1) & 0x3F; //"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]; } else { if(!_spriteInRange && _scanline >= _oamCopybuffer && _scanline < _oamCopybuffer + (_control.LargeSprites ? 16 : 8)) { _spriteInRange = !_oamCopyDone; } if(_secondaryOamAddr < 0x20) { //Copy 1 byte to secondary OAM _secondarySpriteRam[_secondaryOamAddr] = _oamCopybuffer; if(_spriteInRange) { if(_cycle == 66) { //If the first Y coordinate we load is in range, set the sprite 0 flag //This happens even if this isn't actually the first sprite in OAM //(i.e because OAMADDR was not 0 when evaluation started) _sprite0Added = true; } spriteAddrL++; _secondaryOamAddr++; 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 //to replicate a hardware bug noticed in oam_flicker_test_reenable when disabling & re-enabling //rendering on a single scanline //TODO This should actually be using a timer (which runs even with rendering off) rather than the OAM2 address. if((_secondaryOamAddr & 0x03) == 0) { //Done copying all 4 bytes _spriteInRange = false; _lastVisibleSpriteAddr = (spriteAddrH - 1) * 4; if(spriteAddrL != 0) { //Normally, if the sprite eval started on a non-multiple-of-4 address, it would //resync here and start reading the first byte of next entry as the Y value. //But if the last byte read/copied, interpreted as a Y coordinate, has a value //that makes it fall "in range" with the current scanline, then the lower 2 bits //of the address aren't reset, which means the next sprite will also be interpreted incorrectly bool inRange = (_scanline >= _oamCopybuffer && _scanline < _oamCopybuffer + (_control.LargeSprites ? 16 : 8)); if(!inRange) { spriteAddrL = 0; } } } } else { //Nothing to copy, skip to next sprite spriteAddrH = (spriteAddrH + 1) & 0x3F; spriteAddrL = 0; 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]; //8 sprites have been found, check next sprite for overflow + emulate PPU bug if(_oamCopyDone) { //Skip to next sprite (this scenario only happens when the X=255 sprite bug emulation is enabled) spriteAddrH = (spriteAddrH + 1) & 0x3F; spriteAddrL = 0; } else 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; } } } } _spriteRamAddr = (spriteAddrL & 0x03) | (spriteAddrH << 2); } } } template uint8_t NesPpu::ReadSpriteRam(uint8_t addr) { if(!_enableOamDecay) { return _spriteRam[addr]; } else { uint64_t elapsedCycles = _console->GetCpu()->GetCycleCount() - _oamDecayCycles[addr >> 3]; if(elapsedCycles <= NesPpu::OamDecayCycleCount) { _oamDecayCycles[addr >> 3] = _console->GetCpu()->GetCycleCount(); } else { if(_mask.SpritesEnabled) { //When debugging with the break on decayed oam read flag turned on, break (only if sprite rendering is enabled to avoid false positives) _emu->BreakIfDebugging(CpuType::Nes, BreakSource::NesBreakOnDecayedOamRead); } //If this 8-byte row hasn't been read/written to in over 3000 cpu cycles (~1.7ms), //decay the row (set it to addr, clear the bits that don't exist on some bytes) for(int i = 0; i < 8; i++) { int sprAddr = (addr & 0xF8) | i; _spriteRam[sprAddr] = (sprAddr & 0x03) == 0x02 ? (sprAddr & 0xE3) : sprAddr; } } return _spriteRam[addr]; } } template void NesPpu::WriteSpriteRam(uint8_t addr, uint8_t value) { _spriteRam[addr] = value; if(_enableOamDecay) { _oamDecayCycles[addr >> 3] = _console->GetCpu()->GetCycleCount(); } } template uint16_t* NesPpu::GetScreenBuffer(bool previousBuffer, bool processGrayscaleEmphasisBits) { if(!previousBuffer && processGrayscaleEmphasisBits) { UpdateGrayscaleAndIntensifyBits(); } return previousBuffer ? ((_currentOutputBuffer == _outputBuffers[0]) ? _outputBuffers[1] : _outputBuffers[0]) : _currentOutputBuffer; } template void NesPpu::DebugCopyOutputBuffer(uint16_t* target) { memcpy(target, _currentOutputBuffer, NesConstants::ScreenPixelCount * sizeof(uint16_t)); } template void NesPpu::SendFrame() { UpdateGrayscaleAndIntensifyBits(); _emu->ProcessEvent(EventType::EndFrame); void* frameData = ((T*)this)->OnBeforeSendFrame(); if(_console->IsVsMainConsole()) { _emu->GetNotificationManager()->SendNotification(ConsoleNotificationType::PpuFrameDone, _currentOutputBuffer); } //Get phase at the start of the current frame (341*241 cycles ago) uint32_t videoPhase = ((_masterClock / _masterClockDivider) - 82181) % 3; NesConfig& cfg = _console->GetNesConfig(); if(_region != ConsoleRegion::Ntsc || cfg.PpuExtraScanlinesAfterNmi != 0 || cfg.PpuExtraScanlinesBeforeNmi != 0) { //Force 2-phase pattern for PAL or when overclocking is used videoPhase = _frameCount & 0x01; } RenderedFrame frame(_currentOutputBuffer, NesConstants::ScreenWidth, NesConstants::ScreenHeight, 1.0, _frameCount, _console->GetControlManager()->GetPortStates(), videoPhase); frame.Data = frameData; //HD packs if(_console->GetVsMainConsole() || _console->GetVsSubConsole()) { SendFrameVsDualSystem(frame); if(_console->IsVsMainConsole()) { _emu->ProcessEndOfFrame(); } } else { bool forRewind = _emu->GetRewindManager()->IsRewinding(); _emu->GetVideoDecoder()->UpdateFrame(frame, forRewind, forRewind); _emu->ProcessEndOfFrame(); } _enableOamDecay = _settings->GetNesConfig().EnableOamDecay; } template void NesPpu::SendFrameVsDualSystem(RenderedFrame& frame) { NesConfig& cfg = _settings->GetNesConfig(); bool forRewind = _emu->GetRewindManager()->IsRewinding(); if(cfg.VsDualVideoOutput == VsDualOutputOption::MainSystemOnly && _console->IsVsMainConsole()) { _emu->GetVideoDecoder()->UpdateFrame(frame, forRewind, forRewind); } else if(cfg.VsDualVideoOutput == VsDualOutputOption::SubSystemOnly && !_console->IsVsMainConsole()) { _emu->GetVideoDecoder()->UpdateFrame(frame, forRewind, forRewind); } else if(cfg.VsDualVideoOutput == VsDualOutputOption::Both) { if(_console->IsVsMainConsole()) { VideoMergeResult merged = AvMergeUtilities::MergeFrames(frame, ((NesPpu*)_console->GetVsSubConsole()->GetPpu())->_currentOutputBuffer); _emu->GetVideoDecoder()->UpdateFrame(merged.Frame, true, forRewind); } } } template void NesPpu::BeginVBlank() { TriggerNmi(); } template void NesPpu::TriggerNmi() { if(_control.NmiOnVerticalBlank) { _console->GetCpu()->SetNmiFlag(); } } template void NesPpu::UpdateApuStatus() { NesApu* apu = _console->GetApu(); bool enabled = true; if(_scanline > 240) { if(_scanline > _standardVblankEnd) { //Disable APU for extra lines after NMI enabled = false; } else if(_scanline >= _standardNmiScanline && _scanline < _nmiScanline) { //Disable APU for extra lines before NMI enabled = false; } } apu->SetApuStatus(enabled); } template void NesPpu::DebugUpdateFrameBuffer(bool toGrayscale) { //Clear output buffer for "Draw partial frame" feature if(toGrayscale) { for(int i = 0; i < NesConstants::ScreenPixelCount; i++) { _currentOutputBuffer[i] &= 0x30; } } else { memset(_currentOutputBuffer, 0, NesConstants::ScreenPixelCount * 2); } } template void NesPpu::Exec() { if(_cycle < 340) { //Process cycles 1 to 340 _cycle++; if(_scanline < 240) { ((T*)this)->ProcessScanline(); } else if(_cycle == 1 && _scanline == _nmiScanline) { if(!_preventVblFlag) { _statusFlags.VerticalBlank = true; BeginVBlank(); } _preventVblFlag = false; } else if(_region == ConsoleRegion::Pal && _scanline >= _palSpriteEvalScanline) { //"During scanlines 265-310, the PAL PPU does not perform sprite evaluation at all - instead, it simply increments the OAM address register //every 2 pixels (at x=2, x=4, x=6, x=8, ..., x=334, x=336, x=338, and x=340, but not at x=0)." //This code only runs for dots 1-340, so we don't need to check dot 0 explicitly. if(!(_cycle & 1)) { _spriteRamAddr++; if(_enableOamDecay) { _oamDecayCycles[_spriteRamAddr >> 3] = _console->GetCpu()->GetCycleCount(); } } } } else { //Process cycle 0 ProcessScanlineFirstCycle(); } if(_needStateUpdate) { UpdateState(); } _emu->ProcessPpuCycle(); } template void NesPpu::ProcessScanlineFirstCycle() { _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; _emu->ProcessEvent(EventType::StartFrame); UpdateMinimumDrawCycles(); } UpdateApuStatus(); if(_scanline == _console->GetNesConfig().InputScanline) { _console->GetControlManager()->UpdateControlDevices(); _console->GetControlManager()->UpdateInputState(); } //Cycle = 0 if(_scanline < 240) { std::sort(_spriteShifterList, _spriteShifterList + 8); _nextSpriteShifter = 0; _nextSpriteShifterCycle = (_spriteShifterList[0] >> 4); if(_scanline == -1) { _statusFlags.SpriteOverflow = false; _statusFlags.Sprite0Hit = false; _allowFullPpuAccess = true; //2C02E+ bug where OAM reliably corrupts if the row changes at the start of pre-render. Required for Huge Insect. if(IsRenderingEnabled() && !_settings->GetNesConfig().DisableOamAddrBug) { CorruptOamRow(_spriteRamAddr >> 3, _secondaryOamAddr & 0x1F); } //Switch to alternate output buffer (VideoDecoder may still be decoding the last frame buffer) _currentOutputBuffer = (_currentOutputBuffer == _outputBuffers[0]) ? _outputBuffers[1] : _outputBuffers[0]; _emu->AddDebugEvent(DebugEventType::BgColorChange); } else if(_prevRenderingEnabled) { _secondaryOamAddr = 0; if(_scanline > 0 || (!(_frameCount & 0x01) || _region != ConsoleRegion::Ntsc || GetPpuModel() != PpuModel::Ppu2C02)) { //Set bus address to the tile address calculated from the unused NT fetches at the end of the previous scanline //This doesn't happen on scanline 0 if the last dot of the previous frame was skipped SetBusAddress((_tile.TileAddr << 4) | (_videoRamAddr >> 12) | _control.BackgroundPatternAddr); } } } else if(_scanline == 240) { if(_prevRenderingEnabled) { _secondaryOamAddr = 0; } //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 0 for performance reasons SetBusAddress(_videoRamAddr & 0x3FFF); _emu->AddDebugEvent(DebugEventType::BgColorChange); SendFrame(); _frameCount++; } //Refresh the decay timer for the current OAM1 row if in vblank/fblank. //This happens every dot on hardware, but doing it once per scanline should be more than enough while reducing performance cost. if(_enableOamDecay && (_scanline >= 240 || !IsRenderingEnabled())) { _oamDecayCycles[_spriteRamAddr >> 3] = _console->GetCpu()->GetCycleCount(); } } template void NesPpu::CorruptOamRow(uint8_t sourceRow, uint8_t destRow) { if(sourceRow != destRow && _region != ConsoleRegion::Pal) { uint8_t sourceByte = sourceRow << 3; uint8_t destByte = destRow << 3; memcpy(_spriteRam + destByte, _spriteRam + sourceByte, 8); _secondarySpriteRam[destRow] = _secondarySpriteRam[sourceRow]; } } template void NesPpu::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) { _emu->AddDebugEvent(DebugEventType::BgColorChange); _prevRenderingEnabled = _renderingEnabled; if(_scanline < 240) { if(_settings->GetNesConfig().EnablePpuOamRowCorruption && (_cycle >= 257 || (_cycle & 1) == 0)) { if(_renderingEnabled) { CorruptOamRow(_spriteRamAddr >> 3, _secondaryOamAddr & 0x1F); //OAM1 row to OAM2 row. } else { CorruptOamRow(_secondaryOamAddr & 0x1F, _spriteRamAddr >> 3); //OAM2 row to OAM1 row. } } if(!_prevRenderingEnabled) { //When rendering is disabled midscreen, set the vram bus back to the value of 'v' SetBusAddress(_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 _spriteRamAddr++; } } } } if(_renderingEnabled != (_mask.BackgroundEnabled | _mask.SpritesEnabled)) { _renderingEnabled = _mask.BackgroundEnabled | _mask.SpritesEnabled; _needStateUpdate = true; } if(_updateVramAddrDelay > 0) { _updateVramAddrDelay--; if(_updateVramAddrDelay == 0) { if(_console->GetNesConfig().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) { _videoRamAddr &= _updateVramAddr; _emu->BreakIfDebugging(CpuType::Nes, BreakSource::NesBreakOnPpuScrollGlitch); } else if(_cycle > 0 && (_cycle & 0x07) == 0 && (_cycle <= 256 || _cycle > 320)) { _videoRamAddr = (_updateVramAddr & ~0x41F) | (_videoRamAddr & _updateVramAddr & 0x41F); _emu->BreakIfDebugging(CpuType::Nes, BreakSource::NesBreakOnPpuScrollGlitch); } else { _videoRamAddr = _updateVramAddr; } } else { _videoRamAddr = _updateVramAddr; } //The glitches updates corrupt both V and T, so set the new value of V back into T _tmpVideoRamAddr = _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(_videoRamAddr & 0x3FFF); } _emu->AddDebugEvent(DebugEventType::BgColorChange); } else { _needStateUpdate = true; } } if(_ignoreVramRead > 0) { _ignoreVramRead--; if(_ignoreVramRead > 0) { _needStateUpdate = true; } } if(_needVideoRamIncrement) { //Delay vram address increment by 1 ppu cycle after a read/write to 2007 //This allows the full_palette tests to properly display single-pixel glitches //that display the "wrong" color on the screen until the increment occurs (matches hardware) _needVideoRamIncrement = false; UpdateVideoRamAddr(); } if(_ppuMemoryDataReadStateMachine > 0) { _ppuMemoryDataReadStateMachine--; if(_ppuMemoryDataReadStateMachine == 0) { _memoryReadBuffer = ReadVram(_ppuBusAddress & 0x3FFF, MemoryOperationType::Read); _needVideoRamIncrement = true; } _needStateUpdate = true; } if(_ppuMemoryDataWriteStateMachine > 0) { _ppuMemoryDataWriteStateMachine--; if(_ppuMemoryDataWriteStateMachine == 0) { if((_ppuBusAddress & 0x3FFF) >= 0x3F00) { uint8_t value = _ppuMemoryDataWriteLatch; _emu->ProcessPpuWrite(_ppuBusAddress, value, MemoryType::NesPpuMemory); WritePaletteRam(_ppuBusAddress, value); } else { if(_scanline >= 240 || !IsRenderingEnabled()) { _mapper->WriteVram(_ppuBusAddress & 0x3FFF, _ppuMemoryDataWriteLatch); } else { //During rendering, the value written is ignored, and instead the address' LSB is used (not confirmed, based on Visual NES) _mapper->WriteVram(_ppuBusAddress & 0x3FFF, _ppuBusAddress & 0xFF); _emu->BreakIfDebugging(CpuType::Nes, BreakSource::NesInvalidVramAccess); } } _needVideoRamIncrement = true; } _needStateUpdate = true; } if(_dotSkipped) { _dotSkipped--; _needStateUpdate = _needStateUpdate || _dotSkipped; UpdateProcessSpritesFlag(); } } template uint32_t NesPpu::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] & _paletteRamMask) | _intensifyColorBits; return NesDefaultVideoFilter::GetDefaultPixelBrightness(pixelData, GetPpuModel()); } template void NesPpu::Serialize(Serializer& s) { SVArray(_paletteRam, 0x20); SVArray(_spriteRam, 0x100); SVArray(_secondarySpriteRam, 0x20); SVArray(_openBusDecayStamp, 8); SV(_spriteRamAddr); SV(_videoRamAddr); SV(_xScroll); SV(_tmpVideoRamAddr); SV(_writeToggle); SV(_highBitShift); SV(_lowBitShift); SV(_control.VerticalWrite); SV(_control.SpritePatternAddr); SV(_control.BackgroundPatternAddr); SV(_control.LargeSprites); SV(_control.NmiOnVerticalBlank); SV(_mask.Grayscale); SV(_mask.BackgroundMask); SV(_mask.SpriteMask); SV(_mask.BackgroundEnabled); SV(_mask.SpritesEnabled); SV(_mask.IntensifyRed); SV(_mask.IntensifyGreen); SV(_mask.IntensifyBlue); SV(_paletteRamMask); SV(_intensifyColorBits); SV(_statusFlags.SpriteOverflow); SV(_statusFlags.Sprite0Hit); SV(_statusFlags.VerticalBlank); SV(_scanline); SV(_cycle); SV(_frameCount); SV(_memoryReadBuffer); SV(_region); SV(_ppuBusAddress); SV(_masterClock); if(s.GetFormat() != SerializeFormat::Map) { //Hide these entries from the Lua API SV(_currentTilePalette); SV(_tile.LowByte); SV(_tile.HighByte); SV(_tile.PaletteOffset); SV(_tile.TileAddr); SV(_previousTilePalette); SV(_spriteIndex); SV(_spriteCount); SV(_sprite0Added); SV(_sprite0Visible); SV(_oamCopybuffer); SV(_secondaryOamAddr); SV(_spriteInRange); SV(_prevRenderingEnabled); SV(_renderingEnabled); SV(_openBus); SV(_ignoreVramRead); SVArray(_spriteShifterList, 8); SV(_nextSpriteShifter); SV(_nextSpriteShifterCycle); SV(_activeSpriteShifters); SV(_countingSpriteShifters); SV(_expiredSpriteShifters); SV(_dotSkipped); SV(_processSprites); SV(_oamCopyDone); SV(_needStateUpdate); SV(_preventVblFlag); SV(_needVideoRamIncrement); SV(_overflowBugCounter); SV(_updateVramAddr); SV(_updateVramAddrDelay); SV(_allowFullPpuAccess); SV(_ppuMemoryDataReadStateMachine); SV(_ppuMemoryDataWriteStateMachine); SV(_ppuMemoryDataWriteLatch); for(int i = 0; i < _spriteCount; i++) { SVI(_spriteTiles[i].SpriteX); SVI(_spriteTiles[i].LowByte); SVI(_spriteTiles[i].HighByte); SVI(_spriteTiles[i].PaletteOffset); SVI(_spriteTiles[i].BackgroundPriority); } } if(!s.IsSaving()) { UpdateTimings(_region); UpdateMinimumDrawCycles(); UpdateGrayscaleAndIntensifyBits(); 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(); } _spriteShifterList[8] = BaseNesPpu::SpriteShifterDone; _lastUpdatedPixel = -1; UpdateApuStatus(); } } template NesPpu::NesPpu(NesConsole* console); template uint16_t* NesPpu::GetScreenBuffer(bool previousBuffer, bool processGrayscaleEmphasisBits); template void NesPpu::Exec(); template uint32_t NesPpu::GetPixelBrightness(uint8_t x, uint8_t y); template NesPpu::NesPpu(NesConsole* console); template uint16_t* NesPpu::GetScreenBuffer(bool previousBuffer, bool processGrayscaleEmphasisBits); template void NesPpu::Exec(); template uint32_t NesPpu::GetPixelBrightness(uint8_t x, uint8_t y); template NesPpu::NesPpu(NesConsole* console); template uint16_t* NesPpu::GetScreenBuffer(bool previousBuffer, bool processGrayscaleEmphasisBits); template void NesPpu::Exec(); template uint32_t NesPpu::GetPixelBrightness(uint8_t x, uint8_t y); template NesPpu::NesPpu(NesConsole* console); template uint16_t* NesPpu::GetScreenBuffer(bool previousBuffer, bool processGrayscaleEmphasisBits); template void NesPpu::Exec(); template uint32_t NesPpu::GetPixelBrightness(uint8_t x, uint8_t y);