Mesen2/Core/PCE/PceVdc.cpp
Sour 797ef1f57a PCE: Improved timing for SATB/VRAM DMA
Fixes freeze in TV Sports Football's "New Season" screen and is very close to new test rom's timings/results
2025-02-01 19:43:59 +09:00

1490 lines
42 KiB
C++

#include "pch.h"
#include "PCE/PceVdc.h"
#include "PCE/PceVce.h"
#include "PCE/PceVpc.h"
#include "PCE/PceMemoryManager.h"
#include "PCE/PceConstants.h"
#include "PCE/PceConsole.h"
#include "Shared/EmuSettings.h"
#include "Shared/EventType.h"
#include "Shared/MessageManager.h"
#include "Utilities/Serializer.h"
PceVdc::PceVdc(Emulator* emu, PceConsole* console, PceVpc* vpc, PceVce* vce, bool isVdc2)
{
_emu = emu;
_console = console;
_vpc = vpc;
_vce = vce;
_vram = new uint16_t[0x8000];
_spriteRam = new uint16_t[0x100];
console->InitializeRam(_vram, 0x10000);
console->InitializeRam(_spriteRam, 0x200);
//These values can't ever be 0, init them to a possible value
_state.HvLatch.ColumnCount = 32;
_state.HvReg.ColumnCount = 32;
_state.HvLatch.RowCount = 32;
_state.HvReg.RowCount = 32;
_state.VramAddrIncrement = 1;
_state.HvReg.HorizDisplayWidth = 0x1F;
_state.HvLatch.HorizDisplayWidth = 0x1F;
_state.HvReg.VertDisplayWidth = 239;
_state.HvLatch.VertDisplayWidth = 239;
_isVdc2 = isVdc2;
_vramType = isVdc2 ? MemoryType::PceVideoRamVdc2 : MemoryType::PceVideoRam;
_spriteRamType = isVdc2 ? MemoryType::PceSpriteRamVdc2 : MemoryType::PceSpriteRam;
_emu->RegisterMemory(_vramType, _vram, 0x8000 * sizeof(uint16_t));
_emu->RegisterMemory(_spriteRamType, _spriteRam, 0x100 * sizeof(uint16_t));
}
PceVdc::~PceVdc()
{
delete[] _vram;
delete[] _spriteRam;
}
PceVdcState& PceVdc::GetState()
{
return _state;
}
uint8_t PceVdc::GetClockDivider()
{
return _vce->GetClockDivider();
}
uint16_t PceVdc::GetScanlineCount()
{
return _vce->GetScanlineCount();
}
uint16_t PceVdc::DotsToClocks(int dots)
{
return dots * GetClockDivider();
}
void PceVdc::Exec()
{
if(_state.SatbTransferRunning) {
ProcessSatbTransfer();
} else if(_vramDmaRunning) {
ProcessVramDmaTransfer();
}
if(_rowHasSprite0 || _pendingMemoryWrite || _pendingMemoryRead) {
DrawScanline();
if(_pendingMemoryRead || _pendingMemoryWrite) {
//TODO timing, this isn't quite right - should probably be aligned with VDC clocks?
ProcessVramAccesses();
}
}
if(_hModeCounter <= 3 || _nextEventCounter <= 3) {
ProcessVdcEvents();
} else {
_hModeCounter -= 3;
if(_nextEventCounter > 0) {
_nextEventCounter -= 3;
}
_state.HClock += 3;
}
if(_state.HClock == 1365) {
ProcessEndOfScanline();
}
if(!_isVdc2) {
_emu->ProcessPpuCycle<CpuType::Pce>();
}
}
void PceVdc::TriggerHdsIrqs()
{
if(_needVertBlankIrq) {
TriggerVerticalBlank();
}
if(_hasSpriteOverflow && _state.EnableOverflowIrq) {
_state.SpriteOverflow = true;
_vpc->SetIrq(this);
}
_hasSpriteOverflow = false;
}
void PceVdc::ProcessVdcEvents()
{
for(int i = 0; i < 3; i++) {
_state.HClock++;
if(--_hModeCounter == 0) {
DrawScanline();
SetHorizontalMode((PceVdcModeH)(((int)_hMode + 1) % 4));
}
if(_nextEventCounter && --_nextEventCounter == 0) {
ProcessEvent();
}
}
}
void PceVdc::ProcessEvent()
{
DrawScanline();
switch(_nextEvent) {
case PceVdcEvent::LatchScrollY:
_needVCounterClock = true;
_latchClockY = _state.HClock;
IncScrollY();
if(!_state.BurstModeEnabled) {
_state.BackgroundEnabled = _state.NextBackgroundEnabled;
_state.SpritesEnabled = _state.NextSpritesEnabled;
}
_nextEvent = PceVdcEvent::LatchScrollX;
_nextEventCounter = DotsToClocks(2);
break;
case PceVdcEvent::LatchScrollX:
_state.HvLatch.BgScrollX = _state.HvReg.BgScrollX;
_latchClockX = _state.HClock;
_nextEvent = PceVdcEvent::HdsIrqTrigger;
_nextEventCounter = DotsToClocks(6);
break;
case PceVdcEvent::HdsIrqTrigger:
if(_evalStartCycle >= 1365) {
//New row was about to start, but no time to start sprite eval, next row will have no sprites
_spriteCount = 0;
}
TriggerHdsIrqs();
_nextEvent = PceVdcEvent::None;
_nextEventCounter = UINT16_MAX;
break;
case PceVdcEvent::IncRcrCounter:
IncrementRcrCounter();
_nextEvent = PceVdcEvent::None;
_nextEventCounter = UINT16_MAX;
break;
}
}
void PceVdc::SetHorizontalMode(PceVdcModeH hMode)
{
_hMode = hMode;
switch(_hMode) {
case PceVdcModeH::Hds:
_hModeCounter = DotsToClocks((_state.HvLatch.HorizDisplayStart + 1) * 8);
//LogDebug("H: " + std::to_string(_state.HClock) + " - HDS");
break;
case PceVdcModeH::Hdw:
_needVCounterClock = true;
_needRcrIncrement = true;
_nextEvent = PceVdcEvent::IncRcrCounter;
_nextEventCounter = DotsToClocks((_state.HvLatch.HorizDisplayWidth - 1) * 8) + 2;
_hModeCounter = DotsToClocks((_state.HvLatch.HorizDisplayWidth + 1) * 8);
//LogDebug("H: " + std::to_string(_state.HClock) + " - HDW start");
break;
case PceVdcModeH::Hde:
_loadBgStart = UINT16_MAX;
_evalStartCycle = UINT16_MAX;
_loadSpriteStart = _state.HClock;
_hModeCounter = DotsToClocks((_state.HvLatch.HorizDisplayEnd + 1) * 8);
//LogDebug("H: " + std::to_string(_state.HClock) + " - HDE");
break;
case PceVdcModeH::Hsw:
_loadBgStart = UINT16_MAX;
_evalStartCycle = UINT16_MAX;
_hModeCounter = DotsToClocks((_state.HvLatch.HorizSyncWidth + 1) * 8);
_hSyncStartClock = _console->GetMasterClock();
ProcessHorizontalSyncStart();
//LogDebug("H: " + std::to_string(_state.HClock) + " - HSW");
break;
}
}
void PceVdc::ProcessVerticalSyncStart()
{
//Latch VSW/VDS/VDW/VCR at the start of each vertical sync?
_state.HvLatch.VertSyncWidth = _state.HvReg.VertSyncWidth;
_state.HvLatch.VertDisplayStart = _state.HvReg.VertDisplayStart;
_state.HvLatch.VertDisplayWidth = _state.HvReg.VertDisplayWidth;
_state.HvLatch.VertEndPosVcr = _state.HvReg.VertEndPosVcr;
_state.HvLatch.VramAccessMode = _state.HvReg.VramAccessMode;
_state.HvLatch.SpriteAccessMode = _state.HvReg.SpriteAccessMode;
_state.HvLatch.RowCount = _state.HvReg.RowCount;
_state.HvLatch.ColumnCount = _state.HvReg.ColumnCount;
}
void PceVdc::ProcessHorizontalSyncStart()
{
//Latch HSW/HDS/HDW/HDE at the start of each horizontal sync?
_state.HvLatch.HorizSyncWidth = _state.HvReg.HorizSyncWidth;
_state.HvLatch.HorizDisplayStart = _state.HvReg.HorizDisplayStart;
_state.HvLatch.HorizDisplayWidth = _state.HvReg.HorizDisplayWidth;
_state.HvLatch.HorizDisplayEnd = _state.HvReg.HorizDisplayEnd;
_state.HvLatch.CgMode = _state.HvLatch.CgMode;
_nextEvent = PceVdcEvent::None;
_nextEventCounter = UINT16_MAX;
_tileCount = 0;
_screenOffsetX = 0;
uint16_t displayStart = _state.HClock + _hModeCounter + DotsToClocks((_state.HvLatch.HorizDisplayStart + 1) * 8);
if(displayStart - DotsToClocks(24) >= PceConstants::ClockPerScanline) {
return;
}
//Calculate when sprite evaluation, sprite fetching and bg fetching will occur on the scanline
if(_vMode == PceVdcModeV::Vdw || _state.RcrCounter == GetScanlineCount() - 1) {
uint16_t displayWidth = DotsToClocks((_state.HvLatch.HorizDisplayWidth + 1) * 8);
//Sprite evaluation runs on all visible scanlines + the scanline before the picture starts
uint16_t spriteEvalStart = displayStart - DotsToClocks(16);
_evalStartCycle = spriteEvalStart;
_evalLastCycle = 0;
_evalEndCycle = std::min<uint16_t>(PceConstants::ClockPerScanline, spriteEvalStart + displayWidth + DotsToClocks(8));
if(_vMode == PceVdcModeV::Vdw) {
//Turn on BG tile fetching
uint16_t bgFetchStart = displayStart - DotsToClocks(16);
_loadBgStart = bgFetchStart;
_loadBgLastCycle = 0;
_loadBgEnd = std::min<uint16_t>(PceConstants::ClockPerScanline, bgFetchStart + displayWidth + DotsToClocks(16));
}
}
uint16_t eventClocks;
if(_vMode == PceVdcModeV::Vdw) {
_nextEvent = PceVdcEvent::LatchScrollY;
//Less than 33 causes Asuka 120% to have a flickering line
//Either the RCR interrupt is too early, or the latching was too late
eventClocks = DotsToClocks(33);
} else {
_nextEvent = PceVdcEvent::HdsIrqTrigger;
eventClocks = DotsToClocks(24);
}
if((int16_t)displayStart - (int16_t)eventClocks <= (int16_t)_state.HClock) {
ProcessEvent();
} else {
_nextEventCounter = displayStart - eventClocks - _state.HClock;
}
}
void PceVdc::ProcessSpriteEvaluation()
{
if(_state.HClock < _evalStartCycle || _evalLastCycle >= 64 || _state.BurstModeEnabled) {
return;
}
uint16_t end = (std::min(_evalEndCycle, _state.HClock) - _evalStartCycle) / GetClockDivider() / 4;
if(_evalLastCycle >= end) {
return;
}
//LogDebug("SPR EVAL: " + std::to_string(_evalLastCycle) + " -> " + std::to_string(end - 1));
if(_evalLastCycle == 0) {
LoadSpriteTiles();
_spriteCount = 0;
_spriteRow = (_state.RcrCounter + 1) % GetScanlineCount();
}
bool removeSpriteLimit = _emu->GetSettings()->GetPcEngineConfig().RemoveSpriteLimit;
for(uint16_t i = _evalLastCycle; i < end; i++) {
//4 VDC clocks is taken for each sprite
if(i >= 64) {
break;
}
int16_t y = (int16_t)(_spriteRam[i * 4] & 0x3FF) - 64;
if(_spriteRow < y) {
//Sprite not visible on this line
continue;
}
uint8_t height;
uint16_t flags = _spriteRam[i * 4 + 3];
switch((flags >> 12) & 0x03) {
default:
case 0: height = 16; break;
case 1: height = 32; break;
case 2: case 3: height = 64; break;
}
if(_spriteRow >= y + height) {
//Sprite not visible on this line
continue;
}
uint16_t spriteRow = _spriteRow - y;
bool verticalMirror = (flags & 0x8000) != 0;
bool horizontalMirror = (flags & 0x800) != 0;
int yOffset = 0;
int rowOffset = 0;
if(verticalMirror) {
yOffset = (height - spriteRow - 1) & 0x0F;
rowOffset = (height - spriteRow - 1) >> 4;
} else {
yOffset = spriteRow & 0x0F;
rowOffset = spriteRow >> 4;
}
uint16_t tileIndex = (_spriteRam[i * 4 + 2] & 0x7FF) >> 1;
bool loadSp23 = (_spriteRam[i * 4 + 2] & 0x01) != 0;
uint8_t width = (flags & 0x100) ? 32 : 16;
if(width == 32) {
tileIndex &= ~0x01;
}
if(height == 32) {
tileIndex &= ~0x02;
} else if(height == 64) {
tileIndex &= ~0x06;
}
uint16_t spriteTileY = tileIndex | (rowOffset << 1);
for(int x = 0; x < width; x+=16) {
if(_spriteCount >= 16) {
_hasSpriteOverflow = true;
if(!removeSpriteLimit) {
break;
}
}
int columnOffset;
if(horizontalMirror) {
columnOffset = (width - x - 1) >> 4;
} else {
columnOffset = x >> 4;
}
uint16_t spriteTile = spriteTileY | columnOffset;
_sprites[_spriteCount].Index = i;
_sprites[_spriteCount].X = (int16_t)(_spriteRam[i * 4 + 1] & 0x3FF) - 32 + x;
_sprites[_spriteCount].TileAddress = spriteTile * 64 + yOffset;
_sprites[_spriteCount].HorizontalMirroring = horizontalMirror;
_sprites[_spriteCount].ForegroundPriority = (flags & 0x80) != 0;
_sprites[_spriteCount].Palette = (flags & 0x0F);
_sprites[_spriteCount].LoadSp23 = loadSp23;
_spriteCount++;
}
}
_evalLastCycle = end;
}
template<bool skipRender>
void PceVdc::LoadBackgroundTiles()
{
if(_state.HClock < _loadBgStart || _state.BurstModeEnabled) {
return;
}
uint16_t end = (std::min(_loadBgEnd, _state.HClock) - _loadBgStart) / GetClockDivider();
if(_loadBgLastCycle >= end) {
return;
}
//LogDebug("BG: " + std::to_string(_loadBgLastCycle) + " -> " + std::to_string(end - 1));
uint16_t columnMask = _state.HvLatch.ColumnCount - 1;
uint16_t scrollOffset = _state.HvLatch.BgScrollX >> 3;
uint16_t row = (_state.HvLatch.BgScrollY) & ((_state.HvLatch.RowCount * 8) - 1);
if(_state.HvLatch.VramAccessMode == 0) {
for(uint16_t i = _loadBgLastCycle; i < end; i++) {
if constexpr(skipRender) {
_allowVramAccess = (i & 0x01) == 0;
} else {
switch(i & 0x07) {
//CPU can access VRAM
case 0: case 2: case 4: case 6: _allowVramAccess = true; break;
case 1: LoadBatEntry(scrollOffset, columnMask, row); break;
case 3: _allowVramAccess = false; break; //Unused BAT read?
case 5: LoadTileDataCg0(row); break;
case 7: LoadTileDataCg1(row); break;
}
}
}
} else if(_state.HvLatch.VramAccessMode == 3) {
//Mode 3 is 4 cycles per read, CPU has no VRAM access, only 2BPP
LoadBackgroundTilesWidth4(end, scrollOffset, columnMask, row);
} else {
//Mode 1/2 are 2 cycles per read
LoadBackgroundTilesWidth2(end, scrollOffset, columnMask, row);
}
_loadBgLastCycle = end;
}
void PceVdc::LoadBackgroundTilesWidth2(uint16_t end, uint16_t scrollOffset, uint16_t columnMask, uint16_t row)
{
for(uint16_t i = _loadBgLastCycle; i < end; i++) {
_allowVramAccess = false;
switch(i & 0x07) {
case 1: LoadBatEntry(scrollOffset, columnMask, row); break;
case 2: _allowVramAccess = true; break; //CPU
case 3: _allowVramAccess = true; break; //CPU
case 5: LoadTileDataCg0(row); break;
case 7: LoadTileDataCg1(row); break;
}
}
}
void PceVdc::LoadBackgroundTilesWidth4(uint16_t end, uint16_t scrollOffset, uint16_t columnMask, uint16_t row)
{
_allowVramAccess = false;
for(uint16_t i = _loadBgLastCycle; i < end; i++) {
switch(i & 0x07) {
case 3: LoadBatEntry(scrollOffset, columnMask, row); break;
case 7:
//Load CG0 or CG1 based on CG mode flag
_tiles[_tileCount].TileData[0] = ReadVram(_tiles[_tileCount].TileAddr + (row & 0x07) + (_state.HvLatch.CgMode ? 8 : 0));
_tiles[_tileCount].TileData[1] = 0;
_tileCount++;
break;
}
}
}
void PceVdc::LoadBatEntry(uint16_t scrollOffset, uint16_t columnMask, uint16_t row)
{
uint16_t tileColumn = (scrollOffset + _tileCount) & columnMask;
uint16_t batEntry = ReadVram((row >> 3) * _state.HvLatch.ColumnCount + tileColumn);
_tiles[_tileCount].Palette = batEntry >> 12;
_tiles[_tileCount].TileAddr = ((batEntry & 0xFFF) * 16);
_allowVramAccess = false;
}
void PceVdc::LoadTileDataCg0(uint16_t row)
{
_tiles[_tileCount].TileData[0] = ReadVram(_tiles[_tileCount].TileAddr + (row & 0x07));
_allowVramAccess = false;
}
void PceVdc::LoadTileDataCg1(uint16_t row)
{
_tiles[_tileCount].TileData[1] = ReadVram(_tiles[_tileCount].TileAddr + (row & 0x07) + 8);
_allowVramAccess = false;
_tileCount++;
}
uint16_t PceVdc::ReadVram(uint16_t addr)
{
if(addr < 0x8000) {
return _vramOpenBus = _vram[addr];
}
//Camp California expects an empty sprite if tile index is out of bounds (>= $200) - this is probably caused by open bus behavior?
//Return last word of the previously loaded VRAM data
_emu->BreakIfDebugging(CpuType::Pce, BreakSource::PceBreakOnInvalidVramAddress);
return _vramOpenBus;
}
void PceVdc::LoadSpriteTiles()
{
_drawSpriteCount = 0;
_rowHasSprite0 = false;
if(_state.BurstModeEnabled || (_loadSpriteStart >= _loadBgStart && _loadSpriteStart < _loadBgEnd)) {
return;
}
bool removeSpriteLimit = _emu->GetSettings()->GetPcEngineConfig().RemoveSpriteLimit;
uint16_t clockCount = _loadSpriteStart > _loadBgStart ? (PceConstants::ClockPerScanline - _loadSpriteStart) + _loadBgStart : (_loadBgStart - _loadSpriteStart);
bool hasSprite0 = false;
memset(_xPosHasSprite, 0, sizeof(_xPosHasSprite));
if(_state.HvLatch.SpriteAccessMode != 1) {
//Modes 0/2/3 load 4 words over 4, 8 or 16 VDC clocks
uint16_t clocksPerSprite;
switch(_state.HvLatch.SpriteAccessMode) {
default: case 0: clocksPerSprite = 4; break;
case 2: clocksPerSprite = 8; break;
case 3: clocksPerSprite = 16; break;
}
_drawSpriteCount = std::min<uint16_t>(_spriteCount, clockCount / GetClockDivider() / clocksPerSprite);
_totalSpriteCount = removeSpriteLimit ? _spriteCount : _drawSpriteCount;
for(int i = 0; i < _totalSpriteCount; i++) {
PceSpriteInfo& spr = _drawSprites[i];
spr = _sprites[i];
memset(_xPosHasSprite + spr.X + 32, 1, 16);
uint16_t addr = spr.TileAddress;
spr.TileData[0] = ReadVram(addr);
spr.TileData[1] = ReadVram(addr + 16);
spr.TileData[2] = ReadVram(addr + 32);
spr.TileData[3] = ReadVram(addr + 48);
hasSprite0 |= spr.Index == 0;
}
} else {
//Mode 1 uses 2BPP sprites, 4 clocks per sprite
_drawSpriteCount = std::min<uint16_t>(_spriteCount, clockCount / GetClockDivider() / 4);
_totalSpriteCount = removeSpriteLimit ? _spriteCount : _drawSpriteCount;
for(int i = 0; i < _totalSpriteCount; i++) {
PceSpriteInfo& spr = _drawSprites[i];
spr = _sprites[i];
memset(_xPosHasSprite + spr.X + 32, 1, 16);
//Load SP0/SP1 or SP2/SP3 based on flag
uint16_t addr = spr.TileAddress + (spr.LoadSp23 ? 32 : 0);
spr.TileData[0] = ReadVram(addr);
spr.TileData[1] = ReadVram(addr + 16);
spr.TileData[2] = 0;
spr.TileData[3] = 0;
hasSprite0 |= spr.Index == 0;
}
}
if(hasSprite0 && _drawSpriteCount > 1) {
//Force VDC emulation to run on each CPU cycle, to ensure any sprite 0 hit IRQ is triggered at the correct time
_rowHasSprite0 = true;
}
}
bool PceVdc::IsDmaAllowed()
{
if(!_allowDma && !_state.BurstModeEnabled) {
//Can't DMA during rendering
return false;
}
if(_hMode == PceVdcModeH::Hsw && _console->GetMasterClock() - _hSyncStartClock <= DotsToClocks(8)) {
//VRAM accesses are blocked during the first 8 dots after horizontal sync,
//which prevents SATB/VRAM DMA from running during that time (based on test rom result)
return false;
}
return true;
}
void PceVdc::ProcessSatbTransfer()
{
if(!IsDmaAllowed()) {
return;
}
//This takes 1024 VDC cycles (so 2048/3072/4096 master clocks depending on VCE/VDC speed)
//1 word transfered every 4 dots (8 to 16 master clocks, depending on VCE clock divider)
_state.SatbTransferNextWordCounter += 3;
if(_state.SatbTransferNextWordCounter / GetClockDivider() >= 4) {
_state.SatbTransferNextWordCounter -= 4 * GetClockDivider();
int i = _state.SatbTransferOffset;
uint16_t value = ReadVram(_state.SatbBlockSrc + i);
_emu->ProcessPpuWrite<CpuType::Pce>(i << 1, value, _spriteRamType);
_emu->ProcessPpuWrite<CpuType::Pce>((i << 1) + 1, value, _spriteRamType);
_spriteRam[i] = value;
_state.SatbTransferOffset++;
if(_state.SatbTransferOffset == 0) {
_state.SatbTransferRunning = false;
if(_state.VramSatbIrqEnabled) {
_state.SatbTransferDone = true;
_vpc->SetIrq(this);
}
}
}
}
void PceVdc::ProcessVramDmaTransfer()
{
if(!IsDmaAllowed()) {
return;
}
_vramDmaPendingCycles += 3;
uint8_t hClocksPerDmaCycle = GetClockDivider() * 2;
while(_vramDmaPendingCycles >= hClocksPerDmaCycle) {
if(_vramDmaReadCycle) {
_vramDmaBuffer = ReadVram(_state.BlockSrc);
_vramDmaReadCycle = false;
} else {
_state.BlockLen--;
if(_state.BlockDst < 0x8000) {
//Ignore writes over $8000
_vram[_state.BlockDst] = _vramDmaBuffer;
}
_state.BlockSrc += (_state.DecrementSrc ? -1 : 1);
_state.BlockDst += (_state.DecrementDst ? -1 : 1);
_vramDmaReadCycle = true;
if(_state.BlockLen == 0xFFFF) {
_vramDmaRunning = false;
_vramDmaPendingCycles = 0;
if(_state.VramVramIrqEnabled) {
_state.VramTransferDone = true;
_vpc->SetIrq(this);
}
break;
}
}
_vramDmaPendingCycles -= hClocksPerDmaCycle;
}
}
void PceVdc::SetVertMode(PceVdcModeV vMode)
{
_vMode = vMode;
switch(_vMode) {
default:
case PceVdcModeV::Vds:
_vModeCounter = _state.HvLatch.VertDisplayStart + 2;
break;
case PceVdcModeV::Vdw:
_allowDma = false;
_vModeCounter = _state.HvLatch.VertDisplayWidth + 1;
_state.RcrCounter = 0;
break;
case PceVdcModeV::Vde:
_vModeCounter = _state.HvLatch.VertEndPosVcr;
break;
case PceVdcModeV::Vsw:
ProcessVerticalSyncStart();
_vModeCounter = _state.HvLatch.VertSyncWidth + 1;
break;
}
}
void PceVdc::ClockVCounter()
{
_vModeCounter--;
if(_vModeCounter == 0) {
SetVertMode((PceVdcModeV)(((int)_vMode + 1) % 4));
}
_needVCounterClock = false;
}
void PceVdc::IncrementRcrCounter()
{
_state.RcrCounter++;
_needRcrIncrement = false;
ClockVCounter();
if(_vMode == PceVdcModeV::Vde && _state.RcrCounter == _state.HvLatch.VertDisplayWidth + 1) {
_needVertBlankIrq = true;
_verticalBlankDone = true;
}
//This triggers ~12 VDC cycles before the end of the visible part of the scanline
if(_state.EnableScanlineIrq && _state.RcrCounter == (int)_state.RasterCompareRegister - 0x40) {
_state.ScanlineDetected = true;
_vpc->SetIrq(this);
}
}
void PceVdc::IncScrollY()
{
if(_state.RcrCounter == 0) {
_state.HvLatch.BgScrollY = _state.HvReg.BgScrollY;
} else {
if(_state.BgScrollYUpdatePending) {
_state.HvLatch.BgScrollY = _state.HvReg.BgScrollY;
_state.BgScrollYUpdatePending = false;
}
_state.HvLatch.BgScrollY++;
}
}
void PceVdc::ProcessEndOfScanline()
{
DrawScanline();
_state.HClock = 0;
_state.Scanline++;
_latchClockX = UINT16_MAX;
_latchClockY = UINT16_MAX;
if(_state.Scanline == 256) {
_state.FrameCount++;
_vpc->SendFrame(this);
} else if(_state.Scanline >= GetScanlineCount()) {
//Update flags that were locked during burst mode
_state.Scanline = 0;
_verticalBlankDone = false;
_state.BurstModeEnabled = !_state.NextBackgroundEnabled && !_state.NextSpritesEnabled;
_state.BackgroundEnabled = _state.NextBackgroundEnabled;
_state.SpritesEnabled = _state.NextSpritesEnabled;
if(!_isVdc2) {
_vpc->ProcessStartFrame();
_emu->ProcessEvent(EventType::StartFrame);
}
}
_vpc->ProcessScanlineStart(this, _state.Scanline);
if(_needRcrIncrement) {
IncrementRcrCounter();
} else if(_needVCounterClock) {
ClockVCounter();
}
if(_hMode == PceVdcModeH::Hdw) {
//Display output was interrupted by hblank, start loading sprites in ~36 dots. (approximate, based on timing test)
//Could be incorrect in some scenarios, needs more testing
_loadSpriteStart = DotsToClocks(36);
}
//VCE sets HBLANK to low every 1365 clocks, interrupting what
//the VDC was doing and starting a HSW phase
if(_hMode != PceVdcModeH::Hsw) {
_hMode = PceVdcModeH::Hsw;
_hSyncStartClock = _console->GetMasterClock();
}
_loadBgStart = UINT16_MAX;
_evalStartCycle = UINT16_MAX;
//The HSW phase appears to be longer in 7mhz mode compared to 5/10mhz modes
//Less than 32 here breaks Camp California and Shapeshifter
_hModeCounter = DotsToClocks(GetClockDivider() == 3 ? 32 : 24);
_xStart = 0;
_lastDrawHClock = 0;
ProcessHorizontalSyncStart();
if(_state.Scanline == GetScanlineCount() - 3) {
//VCE sets VBLANK for 3 scanlines at the end of every frame
SetVertMode(PceVdcModeV::Vsw);
} else if(_state.Scanline == GetScanlineCount() - 2) {
if(!_verticalBlankDone) {
_needVertBlankIrq = true;
}
}
}
void PceVdc::TriggerDmaStart()
{
_allowDma = true;
if(_state.SatbTransferPending || _state.RepeatSatbTransfer) {
_state.SatbTransferPending = false;
_state.SatbTransferRunning = true;
_state.SatbTransferNextWordCounter = 0;
_state.SatbTransferOffset = 0;
}
}
void PceVdc::TriggerVerticalBlank()
{
//End of display, trigger irq
if(_state.EnableVerticalBlankIrq) {
_state.VerticalBlank = true;
_vpc->SetIrq(this);
}
_needVertBlankIrq = false;
//Any pending SATB/VRAM DMA starts at the same time as the vblank irq is triggered
//This fixes the "new season" screen in "TV Sports Football"
TriggerDmaStart();
}
uint8_t PceVdc::GetTilePixelColor(const uint16_t chr0, const uint16_t chr1, const uint8_t shift)
{
uint16_t color = ((chr0 >> shift) & 0x101) | (((chr1 >> shift) & 0x101) << 2);
return (uint8_t)(color | ((color & 0x500) >> 7));
}
uint8_t PceVdc::GetSpritePixelColor(const uint16_t chrData[4], const uint8_t shift)
{
return (
((chrData[0] >> shift) & 0x01) |
(((chrData[1] >> shift) & 0x01) << 1) |
(((chrData[2] >> shift) & 0x01) << 2) |
(((chrData[3] >> shift) & 0x01) << 3)
);
}
void PceVdc::DrawScanline()
{
if(_state.Scanline < 14 || _state.Scanline >= 256) {
//Only 242 rows can be shown
return;
}
ProcessSpriteEvaluation();
bool skipRender = _vpc->IsSkipRenderEnabled();
if(skipRender) {
LoadBackgroundTiles<true>();
} else {
LoadBackgroundTiles<false>();
}
if(_totalSpriteCount > 0) {
if(_rowHasSprite0) {
if(skipRender) {
InternalDrawScanline<true, true, true>();
} else {
InternalDrawScanline<true, true, false>();
}
} else if(!skipRender) {
InternalDrawScanline<true, false, false>();
}
} else if(!skipRender) {
InternalDrawScanline<false, false, false>();
}
}
template<bool hasSprites, bool hasSprite0, bool skipRender>
void PceVdc::InternalDrawScanline()
{
uint16_t* out = _rowBuffer;
uint16_t pixelsToDraw = (_state.HClock - _lastDrawHClock) / GetClockDivider();
uint16_t xStart = _xStart;
uint16_t xMax = _xStart + pixelsToDraw;
bool inPicture = _hMode == PceVdcModeH::Hdw && _tileCount > 0;
if(inPicture && (_state.BackgroundEnabled || _state.SpritesEnabled)) {
PcEngineConfig& cfg = _emu->GetSettings()->GetPcEngineConfig();
bool bgEnabled = _state.BackgroundEnabled && !(_isVdc2 ? cfg.DisableBackgroundVdc2 : cfg.DisableBackground);
bool sprEnabled = _state.SpritesEnabled && !(_isVdc2 ? cfg.DisableSpritesVdc2 : cfg.DisableSprites);
uint16_t grayscaleBit = _vce->IsGrayscale() ? 0x200 : 0;
uint16_t outColor = 0;
uint8_t bgColor = 0;
for(; xStart < xMax; xStart++) {
if constexpr(!skipRender) {
outColor = PceVpc::TransparentPixelFlag | _vce->GetPalette(0);
bgColor = 0;
if(bgEnabled) {
uint16_t screenX = (_state.HvLatch.BgScrollX & 0x07) + _screenOffsetX;
uint16_t column = screenX >> 3;
bgColor = GetTilePixelColor(_tiles[column].TileData[0], _tiles[column].TileData[1], 7 - (screenX & 0x07));
if(bgColor != 0) {
outColor = _vce->GetPalette(_tiles[column].Palette * 16 + bgColor);
}
}
}
if constexpr(hasSprites) {
if(_state.SpritesEnabled && _xPosHasSprite[_screenOffsetX + 32]) {
uint8_t sprColor;
bool checkSprite0Hit = false;
for(uint16_t i = 0; i < _totalSpriteCount; i++) {
int16_t xOffset = _screenOffsetX - _drawSprites[i].X;
if(xOffset >= 0 && xOffset < 16) {
if(!_drawSprites[i].HorizontalMirroring) {
xOffset = 15 - xOffset;
}
sprColor = GetSpritePixelColor(_drawSprites[i].TileData, xOffset);
if(sprColor != 0) {
if constexpr(hasSprite0) {
if(checkSprite0Hit) {
//Note: don't trigger sprite 0 hit for sprites that are drawn because of the "remove sprite limit" option
if(_state.EnableCollisionIrq && i < _drawSpriteCount) {
_state.Sprite0Hit = true;
_vpc->SetIrq(this);
}
} else {
if(sprEnabled && (bgColor == 0 || _drawSprites[i].ForegroundPriority)) {
outColor = PceVpc::SpritePixelFlag | _vce->GetPalette(256 + _drawSprites[i].Palette * 16 + sprColor);
}
}
if(_drawSprites[i].Index == 0) {
checkSprite0Hit = true;
} else {
break;
}
} else {
if(sprEnabled && (bgColor == 0 || _drawSprites[i].ForegroundPriority)) {
outColor = PceVpc::SpritePixelFlag | _vce->GetPalette(256 + _drawSprites[i].Palette * 16 + sprColor);
}
break;
}
}
}
}
}
}
if constexpr(!skipRender) {
out[xStart] = outColor | grayscaleBit;
}
_screenOffsetX++;
}
} else if(inPicture) {
if constexpr(!skipRender) {
uint16_t color = _vce->GetPalette(0);
for(; xStart < xMax; xStart++) {
//In picture, but BG is not enabled, draw bg color
out[xStart] = PceVpc::TransparentPixelFlag | color;
}
}
} else {
if constexpr(!skipRender) {
uint16_t color = _vce->GetPalette(16 * 16);
for(; xStart < xMax; xStart++) {
//Output hasn't started yet, display overscan color
out[xStart] = PceVpc::TransparentPixelFlag | color;
}
}
}
if(_state.HClock == 1365) {
_vpc->ProcessScanlineEnd(this, _state.Scanline, _rowBuffer);
}
_xStart = xStart;
_lastDrawHClock = _state.HClock / GetClockDivider() * GetClockDivider();
}
void PceVdc::ProcessVramRead()
{
_state.ReadBuffer = ReadVram(_state.MemAddrRead);
if(_state.MemAddrRead < 0x8000) {
_emu->ProcessPpuRead<CpuType::Pce>((_state.MemAddrRead << 1), _state.ReadBuffer, _vramType);
_emu->ProcessPpuRead<CpuType::Pce>((_state.MemAddrRead << 1) + 1, _state.ReadBuffer, _vramType);
}
_state.MemAddrRead += _state.VramAddrIncrement;
_pendingMemoryRead = false;
}
void PceVdc::ProcessVramWrite()
{
if(_state.MemAddrWrite < 0x8000) {
//Ignore writes at $8000+
_emu->ProcessPpuWrite<CpuType::Pce>(_state.MemAddrWrite << 1, _state.VramData, _vramType);
_emu->ProcessPpuWrite<CpuType::Pce>((_state.MemAddrWrite << 1) + 1, _state.VramData, _vramType);
_vram[_state.MemAddrWrite] = _state.VramData;
} else {
_emu->BreakIfDebugging(CpuType::Pce, BreakSource::PceBreakOnInvalidVramAddress);
}
_state.MemAddrWrite += _state.VramAddrIncrement;
_pendingMemoryWrite = false;
}
void PceVdc::ProcessVramAccesses()
{
if(_transferDelay) {
_transferDelay -= 3;
if(_transferDelay > 0) {
return;
}
}
_transferDelay = 0;
bool inBgFetch = !_state.BurstModeEnabled && _state.HClock >= _loadBgStart && _state.HClock < _loadBgEnd && _state.Scanline >= 14 && _state.Scanline < 256;
bool accessBlocked;
if(_vMode != PceVdcModeV::Vdw || _state.BurstModeEnabled || (((!_state.SpritesEnabled || _spriteCount == 0) && !inBgFetch && _vMode == PceVdcModeV::Vdw))) {
//-During SATB/VRAM DMA, prevent all transfers.
//-Allow a VRAM read/write every other dot during:
// -vblank
// -forced blank (burst mode)
// -sprite fetching when no sprites need to be fetched (sprites disabled or no sprites were found during sprite evaluation)
accessBlocked = (_state.SatbTransferRunning || _vramDmaRunning || ((_state.HClock / GetClockDivider()) & 0x01)) ? true : false;
} else {
//During tile/sprite fetching, only allow access on the CPU slots available during background tile fetches
accessBlocked = inBgFetch && !_allowVramAccess;
if(!accessBlocked && !inBgFetch && _state.SpritesEnabled) {
//Find how many clocks have elapsed since sprite fetching started
uint16_t clockCount = _state.HClock > _loadBgEnd ? (_state.HClock - _loadBgEnd) : (PceConstants::ClockPerScanline - _loadBgEnd + _state.HClock);
uint16_t dotCount = clockCount / GetClockDivider();
uint16_t clocksPerSprite;
switch(_state.HvLatch.SpriteAccessMode) {
default: case 0: case 1: clocksPerSprite = 4; break;
case 2: clocksPerSprite = 8; break;
case 3: clocksPerSprite = 16; break;
}
if(dotCount < _spriteCount * clocksPerSprite) {
//VDC is still fetching sprites, block access
accessBlocked = true;
} else {
//Sprite fetching is done, allow access every other dot
accessBlocked = ((_state.HClock / GetClockDivider()) & 0x01) ? true : false;
}
}
}
if(!accessBlocked) {
if(_hMode == PceVdcModeH::Hsw && _console->GetMasterClock() - _hSyncStartClock < 8 * GetClockDivider()) {
//VRAM accesses appear to be blocked during the first 8 dots of horizontal sync
return;
}
if(_pendingMemoryRead) {
ProcessVramRead();
} else if(_pendingMemoryWrite) {
ProcessVramWrite();
}
}
}
void PceVdc::QueueMemoryRead()
{
//All of this is guesswork based on results from a test rom
//Read operations appear to be processed slightly slower than writes?
_pendingMemoryRead = true;
switch(GetClockDivider()) {
case 2: _transferDelay = 15; break; //5 exec ticks
case 3: _transferDelay = 24; break; //8 exec ticks
case 4: _transferDelay = 24; break; //8 exec ticks
}
}
void PceVdc::QueueMemoryWrite()
{
//All of this is guesswork based on results from a test rom
//Write operations appear to be processed slightly faster than reads?
_pendingMemoryWrite = true;
switch(GetClockDivider()) {
case 2: _transferDelay = 12; break; //4 exec ticks
case 3: _transferDelay = 18; break; //6 exec ticks
case 4: _transferDelay = 21; break; //7 exec ticks
}
}
void PceVdc::WaitForVramAccess()
{
//Stall the CPU when a read/write operation is pending
while(_pendingMemoryRead || _pendingMemoryWrite) {
//TODO timing, this is probably not quite right. CPU will be stalled until
//a VDC cycle that allows VRAM access is reached. This isn't always going to
//be a multiple of 3 master clocks like this currently assumes
_console->GetMemoryManager()->ExecFastCycle();
DrawScanline();
}
}
uint8_t PceVdc::ReadRegister(uint16_t addr)
{
DrawScanline();
switch(addr & 0x03) {
default:
case 0: {
uint8_t result = 0;
result |= (_pendingMemoryRead || _pendingMemoryWrite) ? 0x40 : 0x00;
result |= _state.VerticalBlank ? 0x20 : 0x00;
result |= _state.VramTransferDone ? 0x10 : 0x00;
result |= _state.SatbTransferDone ? 0x08 : 0x00;
result |= _state.ScanlineDetected ? 0x04 : 0x00;
result |= _state.SpriteOverflow ? 0x02 : 0x00;
result |= _state.Sprite0Hit ? 0x01 : 0x00;
_state.VerticalBlank = false;
_state.VramTransferDone = false;
_state.SatbTransferDone = false;
_state.ScanlineDetected = false;
_state.SpriteOverflow = false;
_state.Sprite0Hit = false;
_vpc->ClearIrq(this);
return result;
}
case 1: return 0; //Unused, reads return 0
//Reads to 2/3 will always return the read buffer, but the
//read address will only increment when register 2 is selected
case 2:
if(_pendingMemoryRead) {
//D&D Order of the Griffon breaks without this
WaitForVramAccess();
}
return (uint8_t)_state.ReadBuffer;
case 3:
if(_pendingMemoryRead) {
WaitForVramAccess();
}
uint8_t value = _state.ReadBuffer >> 8;
if(_state.CurrentReg == 0x02) {
QueueMemoryRead();
}
return value;
}
}
void PceVdc::WriteRegister(uint16_t addr, uint8_t value)
{
DrawScanline();
switch(addr & 0x03) {
case 0: _state.CurrentReg = value & 0x1F; break;
case 1: break; //Unused, writes do nothing
case 2:
case 3:
bool msb = (addr & 0x03) == 0x03;
switch(_state.CurrentReg) {
case 0x00:
WaitForVramAccess(); //Wonder Momo has graphical issues without this
UpdateReg(_state.MemAddrWrite, value, msb);
break;
case 0x01:
WaitForVramAccess();
UpdateReg(_state.MemAddrRead, value, msb);
if(msb) {
QueueMemoryRead();
}
break;
case 0x02:
WaitForVramAccess();
if(msb) {
UpdateReg(_state.VramData, value, true);
QueueMemoryWrite();
} else {
UpdateReg(_state.VramData, value, false);
}
break;
case 0x05:
if(msb) {
//TODO output select
//TODO dram refresh
switch((value >> 3) & 0x03) {
case 0: _state.VramAddrIncrement = 1; break;
case 1: _state.VramAddrIncrement = 0x20; break;
case 2: _state.VramAddrIncrement = 0x40; break;
case 3: _state.VramAddrIncrement = 0x80; break;
}
} else {
_state.EnableCollisionIrq = (value & 0x01) != 0;
_state.EnableOverflowIrq = (value & 0x02) != 0;
_state.EnableScanlineIrq = (value & 0x04) != 0;
_state.EnableVerticalBlankIrq = (value & 0x08) != 0;
_state.OutputVerticalSync = ((value & 0x30) >> 4) >= 2;
_state.OutputHorizontalSync = ((value & 0x30) >> 4) >= 1;
_state.NextSpritesEnabled = (value & 0x40) != 0;
_state.NextBackgroundEnabled = (value & 0x80) != 0;
if(_latchClockY == _state.HClock && !_state.BurstModeEnabled) {
//Write occurred at the same time as the CR latch, update latch too
_state.SpritesEnabled = _state.NextSpritesEnabled;
_state.BackgroundEnabled = _state.NextBackgroundEnabled;
}
}
break;
case 0x06: UpdateReg<0x3FF>(_state.RasterCompareRegister, value, msb); break;
case 0x07:
UpdateReg<0x3FF>(_state.HvReg.BgScrollX, value, msb);
if(_latchClockX == _state.HClock) {
//Write occurred at the same time as the BXR latch, update latch too
_state.HvLatch.BgScrollX = _state.HvReg.BgScrollX;
}
break;
case 0x08:
UpdateReg<0x1FF>(_state.HvReg.BgScrollY, value, msb);
_state.BgScrollYUpdatePending = true;
if(_latchClockY == _state.HClock) {
//Write occurred at the same time as the BYR latch, update latch too
IncScrollY();
}
break;
case 0x09:
if(!msb) {
switch((value >> 4) & 0x03) {
case 0: _state.HvReg.ColumnCount = 32; break;
case 1: _state.HvReg.ColumnCount = 64; break;
case 2: case 3: _state.HvReg.ColumnCount = 128; break;
}
_state.HvReg.RowCount = (value & 0x40) ? 64 : 32;
_state.HvReg.VramAccessMode = value & 0x03;
_state.HvReg.SpriteAccessMode = (value >> 2) & 0x03;
_state.HvReg.CgMode = (value & 0x80) != 0;
}
break;
case 0x0A:
if(msb) {
_state.HvReg.HorizDisplayStart = value & 0x7F;
} else {
_state.HvReg.HorizSyncWidth = value & 0x1F;
}
break;
case 0x0B:
if(msb) {
_state.HvReg.HorizDisplayEnd = value & 0x7F;
} else {
_state.HvReg.HorizDisplayWidth = value & 0x7F;
}
break;
case 0x0C:
if(msb) {
_state.HvReg.VertDisplayStart = value;
} else {
_state.HvReg.VertSyncWidth = value & 0x1F;
}
break;
case 0x0D:
UpdateReg<0x1FF>(_state.HvReg.VertDisplayWidth, value, msb);
break;
case 0x0E:
if(!msb) {
_state.HvReg.VertEndPosVcr = value;
}
break;
case 0x0F:
if(!msb) {
LogDebugIf(_vramDmaRunning, "[VRAM DMA] Write to register while running");
_state.VramSatbIrqEnabled = (value & 0x01) != 0;
_state.VramVramIrqEnabled = (value & 0x02) != 0;
_state.DecrementSrc = (value & 0x04) != 0;
_state.DecrementDst = (value & 0x08) != 0;
_state.RepeatSatbTransfer = (value & 0x10) != 0;
}
break;
case 0x10:
LogDebugIf(_vramDmaRunning, "[VRAM DMA] Write to register while running");
UpdateReg(_state.BlockSrc, value, msb);
break;
case 0x11:
LogDebugIf(_vramDmaRunning, "[VRAM DMA] Write to register while running");
UpdateReg(_state.BlockDst, value, msb);
break;
case 0x12:
LogDebugIf(_vramDmaRunning, "[VRAM DMA] Write to register while running");
UpdateReg(_state.BlockLen, value, msb);
if(msb) {
_vramDmaRunning = true;
_vramDmaReadCycle = true;
}
break;
case 0x13:
LogDebugIf(_state.SatbTransferRunning, "[Sprite DMA] Write to register while running");
UpdateReg(_state.SatbBlockSrc, value, msb);
if(msb) {
_state.SatbTransferPending = true;
}
break;
}
}
}
void PceVdc::Serialize(Serializer& s)
{
SVArray(_vram, 0x8000);
SVArray(_spriteRam, 0x100);
SVArray(_rowBuffer, PceConstants::MaxScreenWidth);
SV(_state.FrameCount);
SV(_state.HClock);
SV(_state.Scanline);
SV(_state.RcrCounter);
SV(_state.CurrentReg);
SV(_state.MemAddrWrite);
SV(_state.MemAddrRead);
SV(_state.ReadBuffer);
SV(_state.VramData);
SV(_state.EnableCollisionIrq);
SV(_state.EnableOverflowIrq);
SV(_state.EnableScanlineIrq);
SV(_state.EnableVerticalBlankIrq);
SV(_state.OutputVerticalSync);
SV(_state.OutputHorizontalSync);
SV(_state.SpritesEnabled);
SV(_state.BackgroundEnabled);
SV(_state.VramAddrIncrement);
SV(_state.RasterCompareRegister);
if(!s.IsSaving() && s.ContainsKey("ColumnCount")) {
//Backward-compatibility for older save states
s.Stream(_state.HvReg.ColumnCount, "ColumnCount");
s.Stream(_state.HvReg.RowCount, "RowCount");
s.Stream(_state.HvReg.SpriteAccessMode, "SpriteAccessMode");
s.Stream(_state.HvReg.VramAccessMode, "VramAccessMode");
s.Stream(_state.HvReg.CgMode, "CgMode");
_state.HvLatch.ColumnCount = _state.HvReg.ColumnCount;
_state.HvLatch.RowCount = _state.HvReg.RowCount;
_state.HvLatch.SpriteAccessMode = _state.HvReg.SpriteAccessMode;
_state.HvLatch.VramAccessMode = _state.HvReg.VramAccessMode;
_state.HvLatch.CgMode = _state.HvReg.CgMode;
} else {
SV(_state.HvLatch.ColumnCount);
SV(_state.HvLatch.RowCount);
SV(_state.HvLatch.SpriteAccessMode);
SV(_state.HvLatch.VramAccessMode);
SV(_state.HvLatch.CgMode);
SV(_state.HvReg.ColumnCount);
SV(_state.HvReg.RowCount);
SV(_state.HvReg.SpriteAccessMode);
SV(_state.HvReg.VramAccessMode);
SV(_state.HvReg.CgMode);
}
SV(_state.BgScrollYUpdatePending);
SV(_state.HvLatch.BgScrollX);
SV(_state.HvLatch.BgScrollY);
SV(_state.HvLatch.HorizDisplayEnd);
SV(_state.HvLatch.HorizDisplayStart);
SV(_state.HvLatch.HorizDisplayWidth);
SV(_state.HvLatch.HorizSyncWidth);
SV(_state.HvLatch.VertDisplayStart);
SV(_state.HvLatch.VertDisplayWidth);
SV(_state.HvLatch.VertEndPosVcr);
SV(_state.HvLatch.VertSyncWidth);
SV(_state.HvReg.BgScrollX);
SV(_state.HvReg.BgScrollY);
SV(_state.HvReg.HorizDisplayEnd);
SV(_state.HvReg.HorizDisplayStart);
SV(_state.HvReg.HorizDisplayWidth);
SV(_state.HvReg.HorizSyncWidth);
SV(_state.HvReg.VertDisplayStart);
SV(_state.HvReg.VertDisplayWidth);
SV(_state.HvReg.VertEndPosVcr);
SV(_state.HvReg.VertSyncWidth);
SV(_state.VramSatbIrqEnabled);
SV(_state.VramVramIrqEnabled);
SV(_state.DecrementSrc);
SV(_state.DecrementDst);
SV(_state.RepeatSatbTransfer);
SV(_state.BlockSrc);
SV(_state.BlockDst);
SV(_state.BlockLen);
SV(_state.SatbBlockSrc);
SV(_state.SatbTransferPending);
SV(_state.SatbTransferRunning);
SV(_state.SatbTransferNextWordCounter);
SV(_state.SatbTransferOffset);
SV(_state.VerticalBlank);
SV(_state.VramTransferDone);
SV(_state.SatbTransferDone);
SV(_state.ScanlineDetected);
SV(_state.SpriteOverflow);
SV(_state.Sprite0Hit);
SV(_state.BurstModeEnabled);
SV(_state.NextSpritesEnabled);
SV(_state.NextBackgroundEnabled);
if(s.GetFormat() != SerializeFormat::Map) {
//Hide these entries from the Lua API
SV(_vramOpenBus);
SV(_lastDrawHClock);
SV(_xStart);
SV(_hMode);
SV(_hModeCounter);
SV(_vMode);
SV(_vModeCounter);
SV(_screenOffsetX);
SV(_needRcrIncrement);
SV(_needVCounterClock);
SV(_needVertBlankIrq);
SV(_verticalBlankDone);
SV(_spriteCount);
SV(_spriteRow);
SV(_evalStartCycle);
SV(_evalEndCycle);
SV(_evalLastCycle);
SV(_hasSpriteOverflow);
SV(_loadBgStart);
SV(_loadBgEnd);
SV(_loadBgLastCycle);
SV(_tileCount);
SV(_allowVramAccess);
SV(_pendingMemoryRead);
SV(_pendingMemoryWrite);
SV(_transferDelay);
SV(_vramDmaRunning);
SV(_vramDmaReadCycle);
SV(_vramDmaBuffer);
SV(_vramDmaPendingCycles);
SV(_nextEvent);
SV(_nextEventCounter);
SV(_hSyncStartClock);
SV(_allowDma);
SV(_drawSpriteCount);
SV(_totalSpriteCount);
SV(_rowHasSprite0);
SV(_loadSpriteStart);
SV(_latchClockX);
SV(_latchClockY);
SVArray(_xPosHasSprite, sizeof(_xPosHasSprite));
for(int i = 0; i < _spriteCount; i++) {
SVI(_sprites[i].X);
SVI(_sprites[i].TileAddress);
SVI(_sprites[i].Index);
SVI(_sprites[i].Palette);
SVI(_sprites[i].HorizontalMirroring);
SVI(_sprites[i].ForegroundPriority);
SVI(_sprites[i].LoadSp23);
}
for(int i = 0; i < _totalSpriteCount; i++) {
SVI(_drawSprites[i].TileData[0]);
SVI(_drawSprites[i].TileData[1]);
SVI(_drawSprites[i].TileData[2]);
SVI(_drawSprites[i].TileData[3]);
SVI(_drawSprites[i].X);
SVI(_drawSprites[i].Index);
SVI(_drawSprites[i].Palette);
SVI(_drawSprites[i].HorizontalMirroring);
SVI(_drawSprites[i].ForegroundPriority);
}
for(int i = 0; i < _tileCount; i++) {
SVI(_tiles[i].TileData[0]);
SVI(_tiles[i].TileData[1]);
SVI(_tiles[i].Palette);
SVI(_tiles[i].TileAddr);
}
}
}