mirror of
https://github.com/libretro/Mesen.git
synced 2024-11-27 19:10:21 +00:00
944 lines
29 KiB
C++
944 lines
29 KiB
C++
#include "stdafx.h"
|
|
#include "PPU.h"
|
|
#include "CPU.h"
|
|
#include "EmulationSettings.h"
|
|
#include "VideoDecoder.h"
|
|
#include "Debugger.h"
|
|
|
|
PPU* PPU::Instance = nullptr;
|
|
|
|
PPU::PPU(MemoryManager *memoryManager)
|
|
{
|
|
PPU::Instance = this;
|
|
|
|
_memoryManager = memoryManager;
|
|
_outputBuffers[0] = new uint16_t[256 * 240];
|
|
_outputBuffers[1] = new uint16_t[256 * 240];
|
|
|
|
_currentOutputBuffer = _outputBuffers[0];
|
|
|
|
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));
|
|
memset(_spriteRAM, 0xFF, 0x100);
|
|
memset(_secondarySpriteRAM, 0xFF, 0x20);
|
|
|
|
Reset();
|
|
}
|
|
|
|
PPU::~PPU()
|
|
{
|
|
delete[] _outputBuffers[0];
|
|
delete[] _outputBuffers[1];
|
|
}
|
|
|
|
void PPU::Reset()
|
|
{
|
|
_sprite0HitCycle = -1;
|
|
_renderingEnabled = false;
|
|
|
|
_ignoreVramRead = 0;
|
|
_openBus = 0;
|
|
memset(_openBusDecayStamp, 0, sizeof(_openBusDecayStamp));
|
|
|
|
_state = {};
|
|
_flags = {};
|
|
_statusFlags = {};
|
|
|
|
_scanline = 0;
|
|
_cycle = 0;
|
|
_frameCount = 1;
|
|
_memoryReadBuffer = 0;
|
|
}
|
|
|
|
void PPU::SetNesModel(NesModel model)
|
|
{
|
|
_nesModel = model;
|
|
_vblankEnd = (model == NesModel::NTSC ? 260 : 311);
|
|
}
|
|
|
|
PPUDebugState PPU::GetState()
|
|
{
|
|
PPUDebugState state;
|
|
state.ControlFlags = _flags;
|
|
state.StatusFlags = _statusFlags;
|
|
state.State = _state;
|
|
state.Cycle = _cycle;
|
|
state.Scanline = _scanline;
|
|
return state;
|
|
}
|
|
|
|
void PPU::UpdateVideoRamAddr()
|
|
{
|
|
if(_scanline >= 239 || !IsRenderingEnabled()) {
|
|
_state.VideoRamAddr += _flags.VerticalWrite ? 32 : 1;
|
|
|
|
//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"
|
|
_memoryManager->ReadVRAM(_state.VideoRamAddr);
|
|
} 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 = openBus & 0xFF;
|
|
}
|
|
}
|
|
|
|
uint8_t PPU::ApplyOpenBus(uint8_t mask, uint8_t value)
|
|
{
|
|
SetOpenBus(~mask, value);
|
|
return value | (_openBus & mask);
|
|
}
|
|
|
|
uint8_t PPU::ReadRAM(uint16_t addr)
|
|
{
|
|
uint8_t openBusMask = 0xFF;
|
|
uint8_t returnValue = 0;
|
|
switch(GetRegisterID(addr)) {
|
|
case PPURegisters::Status:
|
|
_state.WriteToggle = false;
|
|
_flags.IntensifyBlue = false;
|
|
UpdateStatusFlag();
|
|
returnValue = _state.Status;
|
|
openBusMask = 0x1F;
|
|
break;
|
|
|
|
case PPURegisters::SpriteData:
|
|
if(_scanline <= 240 && IsRenderingEnabled() && (_cycle >= 257 || _cycle <= 64)) {
|
|
if(_cycle >= 257 && _cycle <= 320) {
|
|
//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];
|
|
}
|
|
returnValue = _oamCopybuffer;
|
|
} else {
|
|
returnValue = _spriteRAM[_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 = _memoryManager->ReadVRAM(_state.VideoRamAddr, MemoryOperationType::Read);
|
|
|
|
if((_state.VideoRamAddr & 0x3FFF) >= 0x3F00) {
|
|
returnValue = ReadPaletteRAM(_state.VideoRamAddr) | (_openBus & 0xC0);
|
|
openBusMask = 0xC0;
|
|
} else {
|
|
openBusMask = 0x00;
|
|
}
|
|
|
|
UpdateVideoRamAddr();
|
|
_ignoreVramRead = 2;
|
|
}
|
|
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:
|
|
SetControlRegister(value);
|
|
break;
|
|
case PPURegisters::Mask:
|
|
SetMaskRegister(value);
|
|
break;
|
|
case PPURegisters::SpriteAddr:
|
|
_state.SpriteRamAddr = value;
|
|
break;
|
|
case PPURegisters::SpriteData:
|
|
if(_spriteDmaCounter > 0) {
|
|
_spriteRAM[_spriteDmaAddr & 0xFF] = value;
|
|
_spriteDmaAddr++;
|
|
_spriteDmaCounter--;
|
|
} else {
|
|
if(_scanline >= 240 || !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;
|
|
}
|
|
_spriteRAM[_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;
|
|
_state.TmpVideoRamAddr = (_state.TmpVideoRamAddr & ~0x001F) | (value >> 3);
|
|
}
|
|
_state.WriteToggle = !_state.WriteToggle;
|
|
break;
|
|
case PPURegisters::VideoMemoryAddr:
|
|
if(_state.WriteToggle) {
|
|
_state.TmpVideoRamAddr = (_state.TmpVideoRamAddr & ~0x00FF) | value;
|
|
_state.VideoRamAddr = _state.TmpVideoRamAddr;
|
|
|
|
//Trigger memory read when setting the vram address - needed by MMC3 IRQ counter
|
|
//"4) Should be clocked when A12 changes to 1 via $2006 write"
|
|
_memoryManager->ReadVRAM(_state.VideoRamAddr);
|
|
} else {
|
|
_state.TmpVideoRamAddr = (_state.TmpVideoRamAddr & ~0xFF00) | ((value & 0x3F) << 8);
|
|
}
|
|
_state.WriteToggle = !_state.WriteToggle;
|
|
break;
|
|
case PPURegisters::VideoMemoryData:
|
|
if((_state.VideoRamAddr & 0x3FFF) >= 0x3F00) {
|
|
WritePaletteRAM(_state.VideoRamAddr, value);
|
|
} else {
|
|
_memoryManager->WriteVRAM(_state.VideoRamAddr, value);
|
|
}
|
|
UpdateVideoRamAddr();
|
|
break;
|
|
case PPURegisters::SpriteDMA:
|
|
//_spriteDmaAddr & _spriteDmaCounter are probably not the correct solution for this
|
|
//Missing something else in the PPU/CPU DMA logic maybe?
|
|
_spriteDmaCounter = 0x100;
|
|
_spriteDmaAddr = _state.SpriteRamAddr;
|
|
CPU::RunDMATransfer(_spriteRAM, 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] & _paletteRamMask);
|
|
}
|
|
|
|
void PPU::WritePaletteRAM(uint16_t addr, uint8_t value)
|
|
{
|
|
addr &= 0x1F;
|
|
if(addr == 0x10 || addr == 0x14 || addr == 0x18 || addr == 0x1C) {
|
|
addr &= ~0x10;
|
|
}
|
|
_paletteRAM[addr] = value;
|
|
}
|
|
|
|
bool PPU::IsRenderingEnabled()
|
|
{
|
|
return _renderingEnabled;
|
|
}
|
|
|
|
void PPU::SetControlRegister(uint8_t value)
|
|
{
|
|
_state.Control = value;
|
|
|
|
uint8_t nameTable = (_state.Control & 0x03);
|
|
_state.TmpVideoRamAddr = (_state.TmpVideoRamAddr & ~0x0C00) | (nameTable << 10);
|
|
|
|
_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;
|
|
|
|
//"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."
|
|
bool originalVBlank = _flags.VBlank;
|
|
_flags.VBlank = (_state.Control & 0x80) == 0x80;
|
|
|
|
if(!originalVBlank && _flags.VBlank && _statusFlags.VerticalBlank && (_scanline != -1 || _cycle != 0)) {
|
|
CPU::SetNMIFlag();
|
|
}
|
|
if(_scanline == 241 && _cycle < 3 && !_flags.VBlank) {
|
|
CPU::ClearNMIFlag();
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
//"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 {
|
|
//"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);
|
|
}
|
|
}
|
|
|
|
void PPU::UpdateStatusFlag()
|
|
{
|
|
if(_sprite0HitCycle >= 0 && _sprite0HitCycle < (int32_t)_cycle) {
|
|
_statusFlags.Sprite0Hit = true;
|
|
_sprite0HitCycle = -1;
|
|
}
|
|
|
|
_state.Status = ((uint8_t)_statusFlags.SpriteOverflow << 5) |
|
|
((uint8_t)_statusFlags.Sprite0Hit << 6) |
|
|
((uint8_t)_statusFlags.VerticalBlank << 7);
|
|
_statusFlags.VerticalBlank = false;
|
|
|
|
if(_scanline == 241 && _cycle < 3) {
|
|
//"Reading on the same PPU clock or one later reads it as set, clears it, and suppresses the NMI for that frame."
|
|
_statusFlags.VerticalBlank = false;
|
|
CPU::ClearNMIFlag();
|
|
|
|
if(_cycle == 0) {
|
|
//"Reading one PPU clock before reads it as clear and never sets the flag or generates NMI for that frame. "
|
|
_state.Status = ((uint8_t)_statusFlags.SpriteOverflow << 5) | ((uint8_t)_statusFlags.Sprite0Hit << 6);
|
|
}
|
|
}
|
|
}
|
|
|
|
//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::LoadTileInfo()
|
|
{
|
|
if(IsRenderingEnabled()) {
|
|
uint16_t tileIndex, shift;
|
|
switch((_cycle - 1) & 0x07) {
|
|
case 0:
|
|
_previousTile = _currentTile;
|
|
_currentTile = _nextTile;
|
|
|
|
if(_cycle > 1 && _cycle < 256) {
|
|
LoadNextTile();
|
|
}
|
|
|
|
tileIndex = _memoryManager->ReadVRAM(GetNameTableAddr());
|
|
_nextTile.TileAddr = (tileIndex << 4) | (_state.VideoRamAddr >> 12) | _flags.BackgroundPatternAddr;
|
|
_nextTile.OffsetY = _state.VideoRamAddr >> 12;
|
|
break;
|
|
|
|
case 2:
|
|
shift = ((_state.VideoRamAddr >> 4) & 0x04) | (_state.VideoRamAddr & 0x02);
|
|
_nextTile.PaletteOffset = ((_memoryManager->ReadVRAM(GetAttributeAddr()) >> shift) & 0x03) << 2;
|
|
break;
|
|
|
|
case 3:
|
|
_nextTile.LowByte = _memoryManager->ReadVRAM(_nextTile.TileAddr);
|
|
break;
|
|
|
|
case 5:
|
|
_nextTile.HighByte = _memoryManager->ReadVRAM(_nextTile.TileAddr + 8);
|
|
if(_cycle == 334) {
|
|
InitializeShiftRegisters();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void PPU::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;
|
|
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;
|
|
}
|
|
|
|
if((_spriteIndex < _spriteCount || extraSprite) && spriteY < 240) {
|
|
_spriteTiles[_spriteIndex].BackgroundPriority = backgroundPriority;
|
|
_spriteTiles[_spriteIndex].HorizontalMirror = horizontalMirror;
|
|
_spriteTiles[_spriteIndex].VerticalMirror = verticalMirror;
|
|
_spriteTiles[_spriteIndex].PaletteOffset = ((attributes & 0x03) << 2) | 0x10;
|
|
if(extraSprite) {
|
|
//Use DebugReadVRAM for extra sprites to prevent most side-effects.
|
|
_spriteTiles[_spriteIndex].LowByte = _memoryManager->DebugReadVRAM(tileAddr);
|
|
_spriteTiles[_spriteIndex].HighByte = _memoryManager->DebugReadVRAM(tileAddr + 8);
|
|
} else {
|
|
_spriteTiles[_spriteIndex].LowByte = _memoryManager->ReadVRAM(tileAddr);
|
|
_spriteTiles[_spriteIndex].HighByte = _memoryManager->ReadVRAM(tileAddr + 8);
|
|
}
|
|
_spriteTiles[_spriteIndex].TileAddr = tileAddr;
|
|
_spriteTiles[_spriteIndex].OffsetY = lineOffset;
|
|
_spriteTiles[_spriteIndex].SpriteX = spriteX;
|
|
} else if(!extraSprite) {
|
|
//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;
|
|
}
|
|
|
|
_memoryManager->ReadVRAM(tileAddr);
|
|
_memoryManager->ReadVRAM(tileAddr + 8);
|
|
}
|
|
|
|
_spriteIndex++;
|
|
}
|
|
|
|
void PPU::LoadExtraSprites()
|
|
{
|
|
if(_spriteCount == 8 && EmulationSettings::CheckFlag(EmulationFlags::RemoveSpriteLimit)) {
|
|
for(uint32_t i = _overflowSpriteAddr; i < 0x100; i += 4) {
|
|
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);
|
|
_spriteCount++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void PPU::LoadSpriteTileInfo()
|
|
{
|
|
uint8_t *spriteAddr = _secondarySpriteRAM + _spriteIndex * 4;
|
|
LoadSprite(*spriteAddr, *(spriteAddr+1), *(spriteAddr+2), *(spriteAddr+3), false);
|
|
}
|
|
|
|
void PPU::LoadNextTile()
|
|
{
|
|
_state.LowBitShift |= _nextTile.LowByte;
|
|
_state.HighBitShift |= _nextTile.HighByte;
|
|
}
|
|
|
|
void PPU::InitializeShiftRegisters()
|
|
{
|
|
_state.LowBitShift = (_currentTile.LowByte << 8) | _nextTile.LowByte;
|
|
_state.HighBitShift = (_currentTile.HighByte << 8) | _nextTile.HighByte;
|
|
}
|
|
|
|
void PPU::ShiftTileRegisters()
|
|
{
|
|
_state.LowBitShift <<= 1;
|
|
_state.HighBitShift <<= 1;
|
|
}
|
|
|
|
uint32_t PPU::GetPixelColor(uint32_t &paletteOffset)
|
|
{
|
|
uint8_t offset = _state.XScroll;
|
|
uint32_t backgroundColor = 0;
|
|
|
|
if((_cycle > 8 || _flags.BackgroundMask) && _flags.BackgroundEnabled) {
|
|
//BackgroundMask = false: Hide background in leftmost 8 pixels of screen
|
|
backgroundColor = (((_state.LowBitShift << offset) & 0x8000) >> 15) | (((_state.HighBitShift << offset) & 0x8000) >> 14);
|
|
}
|
|
|
|
if((_cycle > 8 || _flags.SpriteMask) && _flags.SpritesEnabled) {
|
|
//SpriteMask = true: Hide sprites in leftmost 8 pixels of screen
|
|
for(uint8_t i = 0; i < _spriteCount; i++) {
|
|
int32_t shift = -((int32_t)_spriteTiles[i].SpriteX - (int32_t)_cycle + 1);
|
|
if(shift >= 0 && shift < 8) {
|
|
_lastSprite = &_spriteTiles[i];
|
|
uint32_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 && backgroundColor != 0 && _sprite0Visible && _cycle != 256 && _flags.BackgroundEnabled && !_statusFlags.Sprite0Hit && _sprite0HitCycle == -1) {
|
|
//"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"
|
|
|
|
//"Sprite zero hits act as if the image starts at cycle 2 (which is the same cycle that the shifters shift for the first time), so the sprite zero flag will be raised at this point at the earliest."
|
|
//Need to set the sprite 0 flag on the next PPU clock
|
|
_sprite0HitCycle = (int32_t)_cycle;
|
|
}
|
|
|
|
if(backgroundColor == 0 || !_spriteTiles[i].BackgroundPriority) {
|
|
//Check sprite priority
|
|
paletteOffset = _lastSprite->PaletteOffset;
|
|
return spriteColor;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
paletteOffset = ((offset + ((_cycle - 1) & 0x07) < 8) ? _previousTile : _currentTile).PaletteOffset;
|
|
return backgroundColor;
|
|
}
|
|
|
|
void PPU::DrawPixel()
|
|
{
|
|
//This is called 3.7 million times per second - needs to be as fast as possible.
|
|
uint16_t &pixel = _currentOutputBuffer[(_scanline << 8) + _cycle - 1];
|
|
|
|
if(IsRenderingEnabled() || ((_state.VideoRamAddr & 0x3F00) != 0x3F00)) {
|
|
uint32_t paletteOffset;
|
|
uint32_t color = GetPixelColor(paletteOffset);
|
|
if(color == 0) {
|
|
pixel = ReadPaletteRAM(0x3F00) | _intensifyColorBits;
|
|
} else {
|
|
pixel = ReadPaletteRAM(0x3F00 + paletteOffset + color) | _intensifyColorBits;
|
|
}
|
|
} 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."
|
|
pixel = ReadPaletteRAM(_state.VideoRamAddr) | _intensifyColorBits;
|
|
}
|
|
}
|
|
|
|
void PPU::ProcessPreVBlankScanline()
|
|
{
|
|
//For pre-render scanline & all visible scanlines
|
|
if(IsRenderingEnabled()) {
|
|
//Update video ram address according to scrolling logic
|
|
if((_cycle > 0 && _cycle < 256 && (_cycle & 0x07) == 0) || _cycle == 328 || _cycle == 336) {
|
|
IncHorizontalScrolling();
|
|
} else if(_cycle == 256) {
|
|
IncVerticalScrolling();
|
|
} else if(_cycle == 257) {
|
|
//copy horizontal scrolling value from t
|
|
_state.VideoRamAddr = (_state.VideoRamAddr & ~0x041F) | (_state.TmpVideoRamAddr & 0x041F);
|
|
}
|
|
}
|
|
|
|
if(_cycle >= 257 && _cycle <= 320) {
|
|
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 - 260) % 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) {
|
|
_spriteIndex = 0;
|
|
}
|
|
}
|
|
} else if(_cycle == 321 && IsRenderingEnabled()) {
|
|
_oamCopybuffer = _secondarySpriteRAM[0];
|
|
}
|
|
}
|
|
|
|
void PPU::ProcessPrerenderScanline()
|
|
{
|
|
ProcessPreVBlankScanline();
|
|
|
|
if(IsRenderingEnabled() && _cycle == 0 && _state.SpriteRamAddr >= 0x08) {
|
|
//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"
|
|
memcpy(_spriteRAM, _spriteRAM + (_state.SpriteRamAddr & 0xF8), 8);
|
|
}
|
|
|
|
if(_cycle == 1) {
|
|
_statusFlags.SpriteOverflow = false;
|
|
_statusFlags.Sprite0Hit = false;
|
|
_statusFlags.VerticalBlank = false;
|
|
}
|
|
|
|
if(_cycle >= 1 && _cycle <= 256) {
|
|
LoadTileInfo();
|
|
} else if(_cycle >= 280 && _cycle <= 304) {
|
|
if(IsRenderingEnabled()) {
|
|
//copy vertical scrolling value from t
|
|
_state.VideoRamAddr = (_state.VideoRamAddr & ~0x7BE0) | (_state.TmpVideoRamAddr & 0x7BE0);
|
|
}
|
|
} else if(_nesModel == NesModel::NTSC && _cycle == 339 && IsRenderingEnabled() && (_frameCount & 0x01)) {
|
|
//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 = -1;
|
|
_scanline = 0;
|
|
} else if(_cycle >= 321 && _cycle <= 336) {
|
|
LoadTileInfo();
|
|
}
|
|
}
|
|
|
|
void PPU::ProcessVisibleScanline()
|
|
{
|
|
if(_cycle > 0 && _cycle <= 256) {
|
|
LoadTileInfo();
|
|
|
|
DrawPixel();
|
|
ShiftTileRegisters();
|
|
|
|
if(IsRenderingEnabled()) {
|
|
CopyOAMData();
|
|
}
|
|
} else if(_cycle >= 321 && _cycle <= 336) {
|
|
LoadTileInfo();
|
|
} else if(_cycle == 340 && _sprite0HitCycle >= 0) {
|
|
//Set sprite 0 hit flag at the end of the scanline if the cycle count was set.
|
|
//This is just a way of not checking & setting the flag every cycle since the flag needs to be set with a 1 cycle delay
|
|
_statusFlags.Sprite0Hit = true;
|
|
_sprite0HitCycle = -1;
|
|
}
|
|
|
|
ProcessPreVBlankScanline();
|
|
}
|
|
|
|
void PPU::CopyOAMData()
|
|
{
|
|
if(_cycle < 65) {
|
|
//Clear secondary OAM at between cycle 0 and 64
|
|
_oamCopybuffer = 0xFF;
|
|
_secondarySpriteRAM[_cycle >> 1] = _oamCopybuffer;
|
|
} else {
|
|
if(_cycle == 65) {
|
|
_sprite0Added = false;
|
|
_spriteInRange = false;
|
|
_secondaryOAMAddr = 0;
|
|
_overflowSpriteAddr = 0;
|
|
|
|
_oamCopyDone = false;
|
|
_spriteAddrH = (_state.SpriteRamAddr >> 2) & 0x3F;
|
|
_spriteAddrL = _state.SpriteRamAddr & 0x03;
|
|
} else if(_cycle == 256) {
|
|
_sprite0Visible = _sprite0Added;
|
|
_spriteCount = (_secondaryOAMAddr >> 2);
|
|
|
|
LoadExtraSprites();
|
|
}
|
|
|
|
if(_cycle & 0x01) {
|
|
//Read a byte from the primary OAM on odd cycles
|
|
_oamCopybuffer = _spriteRAM[(_spriteAddrH << 2) + _spriteAddrL];
|
|
} else {
|
|
if(_oamCopyDone) {
|
|
_spriteAddrH = (_spriteAddrH + 1) & 0x3F;
|
|
} 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;
|
|
|
|
if(_spriteInRange) {
|
|
_spriteAddrL++;
|
|
_secondaryOAMAddr++;
|
|
|
|
if(_spriteAddrH == 0) {
|
|
_sprite0Added = true;
|
|
}
|
|
|
|
if(_spriteAddrL == 4) {
|
|
//Done copying all 4 bytes
|
|
_spriteInRange = false;
|
|
_spriteAddrL = 0;
|
|
_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 {
|
|
//8 sprites have been found, check next sprite for overflow + emulate PPU bug
|
|
if(_overflowSpriteAddr == 0) {
|
|
//Used to remove sprite limit
|
|
_overflowSpriteAddr = _spriteAddrH * 4;
|
|
}
|
|
|
|
if(_spriteInRange) {
|
|
//Sprite is visible, consider this to be an overflow
|
|
_statusFlags.SpriteOverflow = true;
|
|
_spriteAddrL = (_spriteAddrL + 1) & 0x03;
|
|
if(_spriteAddrL == 4) {
|
|
_spriteInRange = false;
|
|
_spriteAddrH = (_spriteAddrH + 1) & 0x3F;
|
|
_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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void PPU::SendFrame()
|
|
{
|
|
MessageManager::SendNotification(ConsoleNotificationType::PpuFrameDone, _currentOutputBuffer);
|
|
|
|
VideoDecoder::GetInstance()->UpdateFrame(_currentOutputBuffer);
|
|
|
|
//Switch output buffer. VideoDecoder will decode the last frame while we build the new one.
|
|
//If VideoDecoder isn't fast enough, UpdateFrame will block until it is ready to accept a new frame.
|
|
_currentOutputBuffer = (_currentOutputBuffer == _outputBuffers[0]) ? _outputBuffers[1] : _outputBuffers[0];
|
|
}
|
|
|
|
void PPU::BeginVBlank()
|
|
{
|
|
if(_cycle == 0) {
|
|
SendFrame();
|
|
_statusFlags.VerticalBlank = true;
|
|
if(_flags.VBlank) {
|
|
CPU::SetNMIFlag();
|
|
}
|
|
}
|
|
}
|
|
|
|
void PPU::EndVBlank()
|
|
{
|
|
if(_cycle == 340) {
|
|
_frameCount++;
|
|
}
|
|
}
|
|
|
|
void PPU::Exec()
|
|
{
|
|
if(_cycle == 340) {
|
|
_cycle = -1;
|
|
|
|
if(_scanline++ == _vblankEnd) {
|
|
_scanline = -1;
|
|
}
|
|
}
|
|
_cycle++;
|
|
|
|
Debugger::ProcessPpuCycle();
|
|
|
|
if(_scanline != -1 && _scanline < 240) {
|
|
ProcessVisibleScanline();
|
|
} else if(_scanline == -1) {
|
|
ProcessPrerenderScanline();
|
|
} else if(_scanline == 241) {
|
|
BeginVBlank();
|
|
} else if(_scanline == _vblankEnd) {
|
|
EndVBlank();
|
|
}
|
|
|
|
//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)
|
|
_renderingEnabled = _flags.BackgroundEnabled || _flags.SpritesEnabled;
|
|
}
|
|
|
|
void PPU::ExecStatic()
|
|
{
|
|
PPU::Instance->Exec();
|
|
PPU::Instance->Exec();
|
|
PPU::Instance->Exec();
|
|
if(PPU::Instance->_ignoreVramRead) {
|
|
PPU::Instance->_ignoreVramRead--;
|
|
}
|
|
if(PPU::Instance->_nesModel == NesModel::PAL && CPU::GetCycleCount() % 5 == 0) {
|
|
//PAL PPU runs 3.2 clocks for every CPU clock, so we need to run an extra clock every 5 CPU clocks
|
|
PPU::Instance->Exec();
|
|
}
|
|
}
|
|
|
|
void PPU::StreamState(bool saving)
|
|
{
|
|
Stream<uint8_t>(_state.Control);
|
|
Stream<uint8_t>(_state.Mask);
|
|
Stream<uint8_t>(_state.Status);
|
|
Stream<uint32_t>(_state.SpriteRamAddr);
|
|
Stream<uint16_t>(_state.VideoRamAddr);
|
|
Stream<uint8_t>(_state.XScroll);
|
|
Stream<uint16_t>(_state.TmpVideoRamAddr);
|
|
Stream<bool>(_state.WriteToggle);
|
|
Stream<uint16_t>(_state.HighBitShift);
|
|
Stream<uint16_t>(_state.LowBitShift);
|
|
|
|
Stream<bool>(_flags.VerticalWrite);
|
|
Stream<uint16_t>(_flags.SpritePatternAddr);
|
|
Stream<uint16_t>(_flags.BackgroundPatternAddr);
|
|
Stream<bool>(_flags.LargeSprites);
|
|
Stream<bool>(_flags.VBlank);
|
|
|
|
Stream<bool>(_flags.Grayscale);
|
|
Stream<bool>(_flags.BackgroundMask);
|
|
Stream<bool>(_flags.SpriteMask);
|
|
Stream<bool>(_flags.BackgroundEnabled);
|
|
Stream<bool>(_flags.SpritesEnabled);
|
|
Stream<bool>(_flags.IntensifyRed);
|
|
Stream<bool>(_flags.IntensifyGreen);
|
|
Stream<bool>(_flags.IntensifyBlue);
|
|
Stream<uint8_t>(_paletteRamMask);
|
|
Stream<uint16_t>(_intensifyColorBits);
|
|
|
|
Stream<bool>(_statusFlags.SpriteOverflow);
|
|
Stream<bool>(_statusFlags.Sprite0Hit);
|
|
Stream<bool>(_statusFlags.VerticalBlank);
|
|
|
|
Stream<int32_t>(_scanline);
|
|
Stream<uint32_t>(_cycle);
|
|
Stream<uint32_t>(_frameCount);
|
|
Stream<uint8_t>(_memoryReadBuffer);
|
|
|
|
StreamArray<uint8_t>(_paletteRAM, 0x20);
|
|
StreamArray<uint8_t>(_spriteRAM, 0x100);
|
|
StreamArray<uint8_t>(_secondarySpriteRAM, 0x20);
|
|
|
|
Stream<uint8_t>(_currentTile.LowByte);
|
|
Stream<uint8_t>(_currentTile.HighByte);
|
|
Stream<uint32_t>(_currentTile.PaletteOffset);
|
|
|
|
Stream<uint8_t>(_nextTile.LowByte);
|
|
Stream<uint8_t>(_nextTile.HighByte);
|
|
Stream<uint32_t>(_nextTile.PaletteOffset);
|
|
Stream<uint16_t>(_nextTile.TileAddr);
|
|
|
|
Stream<uint8_t>(_previousTile.LowByte);
|
|
Stream<uint8_t>(_previousTile.HighByte);
|
|
Stream<uint32_t>(_previousTile.PaletteOffset);
|
|
|
|
for(int i = 0; i < 64; i++) {
|
|
Stream<uint8_t>(_spriteTiles[i].SpriteX);
|
|
Stream<uint8_t>(_spriteTiles[i].LowByte);
|
|
Stream<uint8_t>(_spriteTiles[i].HighByte);
|
|
Stream<uint32_t>(_spriteTiles[i].PaletteOffset);
|
|
Stream<bool>(_spriteTiles[i].HorizontalMirror);
|
|
Stream<bool>(_spriteTiles[i].BackgroundPriority);
|
|
}
|
|
Stream<uint32_t>(_spriteIndex);
|
|
Stream<uint32_t>(_spriteCount);
|
|
Stream<uint32_t>(_secondaryOAMAddr);
|
|
Stream<bool>(_sprite0Visible);
|
|
|
|
Stream<uint8_t>(_oamCopybuffer);
|
|
Stream<bool>(_spriteInRange);
|
|
Stream<bool>(_sprite0Added);
|
|
Stream<int32_t>(_sprite0HitCycle);
|
|
Stream<uint8_t>(_spriteAddrH);
|
|
Stream<uint8_t>(_spriteAddrL);
|
|
Stream<bool>(_oamCopyDone);
|
|
|
|
Stream<NesModel>(_nesModel);
|
|
|
|
Stream<uint16_t>(_spriteDmaAddr);
|
|
Stream<uint16_t>(_spriteDmaCounter);
|
|
|
|
Stream<bool>(_renderingEnabled);
|
|
Stream<uint8_t>(_openBus);
|
|
StreamArray<int32_t>(_openBusDecayStamp, 8);
|
|
|
|
Stream<uint32_t>(_ignoreVramRead);
|
|
|
|
if(!saving) {
|
|
SetNesModel(_nesModel);
|
|
}
|
|
} |