mirror of
https://github.com/libretro/MesenCE.git
synced 2026-11-08 17:26:27 -10:00
Also fixed a few scenarios where the PPU memory write callbacks did not allow the Lua script to change the value about to be written (because the callback was called too late)
1489 lines
43 KiB
C++
1489 lines
43 KiB
C++
#include "pch.h"
|
|
#include "Gameboy/GbPpu.h"
|
|
#include "Gameboy/GbTypes.h"
|
|
#include "Gameboy/Gameboy.h"
|
|
#include "Gameboy/GbMemoryManager.h"
|
|
#include "Gameboy/GbDmaController.h"
|
|
#include "Gameboy/GbConstants.h"
|
|
#include "Shared/Emulator.h"
|
|
#include "Shared/EmuSettings.h"
|
|
#include "Shared/RewindManager.h"
|
|
#include "Shared/BaseControlManager.h"
|
|
#include "Shared/RenderedFrame.h"
|
|
#include "Shared/Video/VideoDecoder.h"
|
|
#include "Shared/NotificationManager.h"
|
|
#include "Shared/MessageManager.h"
|
|
#include "SNES/Coprocessors/SGB/SuperGameboy.h"
|
|
#include "Utilities/HexUtilities.h"
|
|
#include "Utilities/Serializer.h"
|
|
#include "Shared/EventType.h"
|
|
#include "Shared/Utilities/AvMergeUtilities.h"
|
|
|
|
constexpr uint16_t evtColors[6] = { 0x18C6, 0x294A, 0x108C, 0x4210, 0x3084, 0x1184 };
|
|
|
|
void GbPpu::Init(Emulator* emu, Gameboy* gameboy, GbMemoryManager* memoryManager, GbDmaController* dmaController, uint8_t* vram, uint8_t* oam)
|
|
{
|
|
_emu = emu;
|
|
_gameboy = gameboy;
|
|
_memoryManager = memoryManager;
|
|
_dmaController = dmaController;
|
|
_vram = vram;
|
|
_oam = oam;
|
|
_settings = _emu->GetSettings();
|
|
|
|
_outputBuffers[0] = new uint16_t[GbConstants::PixelCount];
|
|
_outputBuffers[1] = new uint16_t[GbConstants::PixelCount];
|
|
memset(_outputBuffers[0], 0, GbConstants::PixelCount * sizeof(uint16_t));
|
|
memset(_outputBuffers[1], 0, GbConstants::PixelCount * sizeof(uint16_t));
|
|
_currentBuffer = _outputBuffers[0];
|
|
|
|
_eventViewerBuffers[0] = new uint16_t[GbConstants::EventViewerBufferSize];
|
|
_eventViewerBuffers[1] = new uint16_t[GbConstants::EventViewerBufferSize];
|
|
memset(_eventViewerBuffers[0], 0, GbConstants::EventViewerBufferSize * sizeof(uint16_t));
|
|
memset(_eventViewerBuffers[1], 0, GbConstants::EventViewerBufferSize * sizeof(uint16_t));
|
|
_currentEventViewerBuffer = _eventViewerBuffers[0];
|
|
|
|
_state = {};
|
|
_state.Mode = PpuMode::HBlank;
|
|
_state.CgbEnabled = _gameboy->IsCgb();
|
|
_lastFrameTime = 0;
|
|
|
|
_gameboy->InitializeRam(_state.CgbBgPalettes, 4 * 8 * sizeof(uint16_t));
|
|
_gameboy->InitializeRam(_state.CgbObjPalettes, 4 * 8 * sizeof(uint16_t));
|
|
if(_state.CgbEnabled) {
|
|
_emu->RegisterMemory(MemoryType::GbBgPaletteRam, _state.CgbBgPalettes, 4 * 8 * sizeof(uint16_t));
|
|
_emu->RegisterMemory(MemoryType::GbObjPaletteRam, _state.CgbObjPalettes, 4 * 8 * sizeof(uint16_t));
|
|
}
|
|
|
|
UpdatePalette();
|
|
|
|
Write(0xFF48, 0xFF);
|
|
Write(0xFF49, 0xFF);
|
|
|
|
//Reset state to ensure powering off and then back on works properly for SGB
|
|
ResetRenderer();
|
|
_wyEnableFlag = false;
|
|
_wxEnableFlag = false;
|
|
_lcdDisabled = true;
|
|
_stopOamBlocked = false;
|
|
_stopVramBlocked = false;
|
|
_stopPaletteBlocked = false;
|
|
_oamReadBlocked = false;
|
|
_oamWriteBlocked = false;
|
|
_vramReadBlocked = false;
|
|
_vramWriteBlocked = false;
|
|
_isFirstFrame = true;
|
|
_forceBlankFrame = true;
|
|
_rendererIdle = false;
|
|
|
|
_overclockScanlineCount = 0;
|
|
_vblankStartScanline = 144;
|
|
_lastScanline = 153;
|
|
}
|
|
|
|
GbPpu::~GbPpu()
|
|
{
|
|
delete[] _outputBuffers[0];
|
|
delete[] _outputBuffers[1];
|
|
delete[] _eventViewerBuffers[0];
|
|
delete[] _eventViewerBuffers[1];
|
|
}
|
|
|
|
GbPpuState GbPpu::GetState()
|
|
{
|
|
return _state;
|
|
}
|
|
|
|
GbPpuState& GbPpu::GetStateRef()
|
|
{
|
|
return _state;
|
|
}
|
|
|
|
uint16_t* GbPpu::GetOutputBuffer()
|
|
{
|
|
return _currentBuffer;
|
|
}
|
|
|
|
uint16_t* GbPpu::GetEventViewerBuffer()
|
|
{
|
|
return _currentEventViewerBuffer;
|
|
}
|
|
|
|
uint16_t* GbPpu::GetPreviousEventViewerBuffer()
|
|
{
|
|
return _currentEventViewerBuffer == _eventViewerBuffers[0] ? _eventViewerBuffers[1] : _eventViewerBuffers[0];
|
|
}
|
|
|
|
void GbPpu::SetCpuStopState(bool stopped)
|
|
{
|
|
if(!_gameboy->IsCgb()) {
|
|
if(stopped) {
|
|
_lcdDisabled = true;
|
|
} else if(_state.LcdEnabled) {
|
|
_lcdDisabled = !_state.LcdEnabled;
|
|
}
|
|
} else {
|
|
if(stopped) {
|
|
//When the CPU is stopped on the CGB, the LCD is still active, but its oam/vram/palette
|
|
//access is frozen to what it was when the CPU stopped. So if stopped during mode 3, the
|
|
//LCD essentially continues rendering the last image that was on screen.
|
|
//If this is done outside of mode 3, oam/vram/pal access can be disabled, causing the
|
|
//screen to turn black, etc.
|
|
_stopOamBlocked = !_oamReadBlocked;
|
|
_stopVramBlocked = !_vramReadBlocked;
|
|
_stopPaletteBlocked = _state.Mode <= PpuMode::OamEvaluation;
|
|
} else {
|
|
_stopOamBlocked = false;
|
|
_stopVramBlocked = false;
|
|
_stopPaletteBlocked = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
template<bool singleStep>
|
|
void GbPpu::Exec()
|
|
{
|
|
if(_lcdDisabled) {
|
|
//LCD is disabled, prevent IRQs, etc.
|
|
//Not quite correct in terms of frame pacing
|
|
if(_gameboy->GetApuCycleCount() - _lastFrameTime > 70224) {
|
|
//More than a full frame's worth of time has passed since the last frame, send another blank frame
|
|
_forceBlankFrame = true;
|
|
SendFrame();
|
|
_forceBlankFrame = true;
|
|
_lastFrameTime = _gameboy->GetApuCycleCount();
|
|
}
|
|
return;
|
|
}
|
|
|
|
uint8_t cyclesToRun = (_memoryManager->IsHighSpeed() || singleStep) ? 1 : 2;
|
|
for(int i = 0; i < cyclesToRun; i++) {
|
|
_state.Cycle++;
|
|
if(_state.IdleCycles > 0) {
|
|
_state.IdleCycles--;
|
|
ProcessPpuCycle();
|
|
continue;
|
|
}
|
|
|
|
ExecCycle();
|
|
}
|
|
}
|
|
|
|
void GbPpu::ExecCycle()
|
|
{
|
|
PpuMode oldMode = _state.IrqMode;
|
|
|
|
if(_state.Scanline < 144) {
|
|
if(_state.Scanline == 0 && _isFirstFrame) {
|
|
ProcessFirstScanlineAfterPowerOn();
|
|
} else {
|
|
ProcessVisibleScanline();
|
|
}
|
|
} else {
|
|
ProcessVblankScanline();
|
|
}
|
|
|
|
if(_state.Mode == PpuMode::Drawing) {
|
|
RunDrawCycle();
|
|
if(_drawnPixels == 160) {
|
|
//Mode turns to hblank on the same cycle as the last pixel is output
|
|
_state.IrqMode = PpuMode::HBlank;
|
|
if(_gameboy->IsSgb()) {
|
|
_gameboy->GetSgb()->ProcessHBlank();
|
|
}
|
|
_state.IdleCycles = 456 - _state.Cycle - 1;
|
|
|
|
_oamReadBlocked = false;
|
|
_oamWriteBlocked = false;
|
|
_vramReadBlocked = false;
|
|
_vramWriteBlocked = false;
|
|
|
|
SetMode(PpuMode::HBlank);
|
|
}
|
|
} else if(_state.Mode == PpuMode::OamEvaluation) {
|
|
RunSpriteEvaluation();
|
|
}
|
|
|
|
bool coincidenceFlag = (_state.LyCompare == _state.LyForCompare);
|
|
if(_state.IrqMode != oldMode || _state.LyCoincidenceFlag != coincidenceFlag) {
|
|
_state.LyCoincidenceFlag = coincidenceFlag;
|
|
UpdateStatIrq();
|
|
}
|
|
|
|
ProcessPpuCycle();
|
|
}
|
|
|
|
void GbPpu::ProcessVblankScanline()
|
|
{
|
|
if(_state.Scanline < _vblankStartScanline) {
|
|
ProcessOverclockScanline();
|
|
return;
|
|
}
|
|
|
|
switch(_state.Cycle) {
|
|
case 2:
|
|
if(_state.Scanline == _vblankStartScanline) {
|
|
_state.IrqMode = PpuMode::OamEvaluation;
|
|
}
|
|
break;
|
|
|
|
case 4:
|
|
if(_state.Scanline < _lastScanline) {
|
|
_state.LyForCompare = _state.Scanline - _overclockScanlineCount;
|
|
if(_state.Scanline == _vblankStartScanline) {
|
|
SetMode(PpuMode::VBlank);
|
|
_state.IrqMode = PpuMode::VBlank;
|
|
if(_gameboy->IsSgb()) {
|
|
_gameboy->GetSgb()->ProcessVBlank();
|
|
}
|
|
_windowCounter = -1;
|
|
_memoryManager->RequestIrq(GbIrqSource::VerticalBlank);
|
|
SendFrame();
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 6:
|
|
if(_state.Scanline == _lastScanline) {
|
|
_state.Ly = 0;
|
|
_state.LyForCompare = _state.Scanline - _overclockScanlineCount;
|
|
}
|
|
break;
|
|
|
|
case 8:
|
|
if(_state.Scanline == _lastScanline) {
|
|
_state.LyForCompare = -1;
|
|
}
|
|
break;
|
|
|
|
case 12:
|
|
if(_state.Scanline == _lastScanline) {
|
|
_state.LyForCompare = 0;
|
|
}
|
|
_state.IdleCycles = 456 - 12 - 1;
|
|
break;
|
|
|
|
case 456:
|
|
_state.Cycle = 0;
|
|
_state.Scanline++;
|
|
_drawnPixels = 0;
|
|
|
|
if(_state.Scanline > _lastScanline) {
|
|
_overclockScanlineCount = _gameboy->IsSgb() ? 0 : _settings->GetGameboyConfig().OverclockScanlineCount;
|
|
_vblankStartScanline = 144 + _overclockScanlineCount;
|
|
_lastScanline = 153 + _overclockScanlineCount;
|
|
|
|
_state.Scanline = 0;
|
|
_state.Ly = 0;
|
|
_state.LyForCompare = 0;
|
|
_wyEnableFlag = _state.WindowY == 0 && _state.WindowEnabled;
|
|
|
|
if(!_gameboy->IsCgb()) {
|
|
//On scanline 0, hblank gets set here (not on CGB)
|
|
SetMode(PpuMode::HBlank);
|
|
}
|
|
|
|
if(_emu->IsDebugging()) {
|
|
_emu->ProcessEvent(EventType::StartFrame, CpuType::Gameboy);
|
|
_currentEventViewerBuffer = _currentEventViewerBuffer == _eventViewerBuffers[0] ? _eventViewerBuffers[1] : _eventViewerBuffers[0];
|
|
}
|
|
} else {
|
|
_state.Ly = _state.Scanline - _overclockScanlineCount;
|
|
_state.LyForCompare = -1;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
void GbPpu::ProcessFirstScanlineAfterPowerOn()
|
|
{
|
|
switch(_state.Cycle) {
|
|
case 87:
|
|
_oamReadBlocked = true;
|
|
_oamWriteBlocked = true;
|
|
_vramReadBlocked = true;
|
|
_vramWriteBlocked = true;
|
|
|
|
_rendererIdle = true;
|
|
_wxEnableFlag = false;
|
|
SetMode(PpuMode::Drawing);
|
|
_state.IrqMode = PpuMode::Drawing;
|
|
ResetRenderer();
|
|
_rendererIdle = true;
|
|
break;
|
|
|
|
case 92:
|
|
_rendererIdle = false;
|
|
break;
|
|
|
|
case 456:
|
|
_state.Cycle = 0;
|
|
_state.Scanline++;
|
|
_wyEnableFlag |= _state.Scanline == _state.WindowY && _state.WindowEnabled;
|
|
_drawnPixels = 0;
|
|
_state.Ly = _state.Scanline;
|
|
break;
|
|
}
|
|
}
|
|
|
|
void GbPpu::ProcessVisibleScanline()
|
|
{
|
|
switch(_state.Cycle) {
|
|
case 2:
|
|
if(_state.Scanline > 0) {
|
|
//On scanlines 1-143, the OAM IRQ fires 1 cycle early
|
|
_state.IrqMode = PpuMode::OamEvaluation;
|
|
_state.LyForCompare = -1;
|
|
}
|
|
break;
|
|
|
|
case 3:
|
|
_oamReadBlocked = true;
|
|
break;
|
|
|
|
case 4:
|
|
_spriteCount = 0;
|
|
_state.LyForCompare = _state.Scanline;
|
|
_oamWriteBlocked = true;
|
|
SetMode(PpuMode::OamEvaluation);
|
|
_state.IrqMode = PpuMode::OamEvaluation;
|
|
break;
|
|
|
|
case 5:
|
|
//Turning on OAM IRQs in the middle of evaluation has no effect?
|
|
//Or is this a patch to get the proper behavior for the STAT write bug?
|
|
_state.IrqMode = PpuMode::NoIrq;
|
|
break;
|
|
|
|
case 80:
|
|
_oamWriteBlocked = false;
|
|
_oamReadBlocked = true;
|
|
_vramReadBlocked = true;
|
|
break;
|
|
|
|
case 84:
|
|
_wxEnableFlag = false;
|
|
SetMode(PpuMode::Drawing);
|
|
_state.IrqMode = PpuMode::Drawing;
|
|
_oamWriteBlocked = true;
|
|
_vramWriteBlocked = true;
|
|
_rendererIdle = true;
|
|
break;
|
|
|
|
case 89:
|
|
ResetRenderer();
|
|
_rendererIdle = false;
|
|
break;
|
|
|
|
case 456:
|
|
_state.Cycle = 0;
|
|
_state.Scanline++;
|
|
_state.Ly = _state.Scanline;
|
|
_wyEnableFlag |= _state.Scanline == _state.WindowY && _state.WindowEnabled;
|
|
_drawnPixels = 0;
|
|
if(_state.Scanline == 144) {
|
|
if(_vblankStartScanline != 144) {
|
|
//Overclocked scanlines behave as if LY was 143 for all of them
|
|
_state.Ly = 143;
|
|
}
|
|
_state.LyForCompare = -1;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
void GbPpu::ProcessOverclockScanline()
|
|
{
|
|
if(_state.Cycle == 456) {
|
|
_state.Cycle = 0;
|
|
_state.Scanline++;
|
|
if(_state.Scanline == _vblankStartScanline) {
|
|
_state.Ly = _state.Scanline - _overclockScanlineCount;
|
|
_state.LyForCompare = -1;
|
|
_gameboy->SetApuEnabled(true);
|
|
}
|
|
} else {
|
|
_gameboy->SetApuEnabled(false);
|
|
_state.IdleCycles = 456 - _state.Cycle - 1;
|
|
}
|
|
}
|
|
|
|
void GbPpu::ProcessPpuCycle()
|
|
{
|
|
_gbcTileGlitch = false;
|
|
|
|
if(_emu->IsDebugging()) {
|
|
_emu->ProcessPpuCycle<CpuType::Gameboy>();
|
|
if(_state.Scanline < 144) {
|
|
if(_state.Mode != PpuMode::Drawing) {
|
|
_currentEventViewerBuffer[456 * _state.Scanline + _state.Cycle] = evtColors[(int)_state.Mode];
|
|
} else if(_prevDrawnPixels != _drawnPixels && _drawnPixels > 0) {
|
|
uint16_t color = _currentBuffer[_state.Scanline * GbConstants::ScreenWidth + (_drawnPixels - 1)];
|
|
_currentEventViewerBuffer[456 * _state.Scanline + _state.Cycle] = color;
|
|
} else {
|
|
_currentEventViewerBuffer[456 * _state.Scanline + _state.Cycle] = evtColors[(int)_evtColor];
|
|
}
|
|
}
|
|
_prevDrawnPixels = _drawnPixels;
|
|
}
|
|
}
|
|
|
|
void GbPpu::RunDrawCycle()
|
|
{
|
|
if(_rendererIdle) {
|
|
//Idle cycles
|
|
_evtColor = EvtColor::RenderingIdle;
|
|
return;
|
|
}
|
|
|
|
//TODO fix/check behavior for WX=0 and WX=166
|
|
if(!_wxEnableFlag) {
|
|
_wxEnableFlag |= _drawnPixels == _state.WindowX - 7;
|
|
|
|
bool fetchWindow =
|
|
_state.WindowEnabled && //"Window enable bit in LCDC is set"
|
|
_wxEnableFlag && //"the current X coordinate being rendered + 7 was equal to WX"
|
|
_wyEnableFlag; //"at some point in this frame the value of WY was equal to LY (checked at the start of Mode 2 only)"
|
|
|
|
if(_fetchWindow != fetchWindow) {
|
|
//Switched between window & background, reset fetcher & pixel FIFO
|
|
_fetchWindow = fetchWindow;
|
|
_fetchColumn = 0;
|
|
_windowCounter++;
|
|
|
|
_bgFetcher.Step = 0;
|
|
_bgFifo.Reset();
|
|
|
|
//Idle cycle when switching to window
|
|
_evtColor = EvtColor::RenderingIdle;
|
|
return;
|
|
}
|
|
|
|
if(!_gameboy->IsCgb() && !_state.WindowEnabled && _windowCounter > 0 && _wxEnableFlag && ((int)_state.WindowX & 0x07) == 7 - ((int)_state.ScrollX & 0x07)) {
|
|
//Emulate a DMG-only glitch that occurs in the following conditions (from the Windesync-validate test rom readme):
|
|
//After the window is active at some point in the frame, an extra pixel is added to the BG FIFO on all other lines when:
|
|
// -The window is disabled AND
|
|
// -A window X hit occurs AND
|
|
// -(WindowX & 7) == 7 - (ScrollX & 7)
|
|
//This glitch also occurs in Star Trek 25th anniversary's intro - not emulating it causes the graphics to be offset by a pixel
|
|
_insertGlitchBgPixel = true;
|
|
}
|
|
}
|
|
|
|
FindNextSprite();
|
|
if(_fetchSprite >= 0 && _bgFetcher.Step >= 5 && _bgFifo.Size > 0) {
|
|
if(_fetchSprite < 10) {
|
|
_evtColor = EvtColor::RenderingOamLoad;
|
|
ClockSpriteFetcher();
|
|
FindNextSprite();
|
|
return;
|
|
} else {
|
|
LoadExtraSprite();
|
|
}
|
|
}
|
|
|
|
if(_fetchSprite == -1 && _bgFifo.Size > 0) {
|
|
if(_drawnPixels >= 0) {
|
|
GameboyConfig& cfg = _emu->GetSettings()->GetGameboyConfig();
|
|
|
|
GbFifoEntry entry = _insertGlitchBgPixel ? GbFifoEntry {} : _bgFifo.Content[_bgFifo.Position];
|
|
GbFifoEntry sprite = _oamFifo.Content[_oamFifo.Position];
|
|
if(!cfg.DisableSprites && sprite.Color != 0 && (entry.Color == 0 || (!(sprite.Attributes & 0x80) && !(entry.Attributes & 0x80)) || (_state.CgbEnabled && !_state.BgEnabled))) {
|
|
//Use sprite pixel if:
|
|
// -BG color is 0, OR
|
|
// -Sprite is background priority AND BG does not have its priority bit set, OR
|
|
// -On CGB, the "bg enabled" flag is cleared, causing all sprites to appear above BG tiles
|
|
if(_state.CgbEnabled) {
|
|
WriteObjPixel(sprite.Color | ((sprite.Attributes & 0x07) << 2));
|
|
} else {
|
|
uint8_t colorIndex = (((sprite.Attributes & 0x10) ? _state.ObjPalette1 : _state.ObjPalette0) >> (sprite.Color * 2)) & 0x03;
|
|
WriteObjPixel(((sprite.Attributes & 0x10) ? 4 : 0) | colorIndex);
|
|
}
|
|
_lastPixelType = GbPixelType::Object;
|
|
} else {
|
|
if(!cfg.DisableBackground) {
|
|
if(_state.CgbEnabled) {
|
|
WriteBgPixel(entry.Color | ((entry.Attributes & 0x07) << 2));
|
|
} else {
|
|
WriteBgPixel(_state.BgEnabled ? ((_state.BgPalette >> (entry.Color * 2)) & 0x03) : (_state.BgPalette & 0x03));
|
|
}
|
|
} else {
|
|
WriteBgPixel(_state.BgPalette & 0x03);
|
|
}
|
|
_lastPixelType = GbPixelType::Background;
|
|
_lastBgColor = entry.Color;
|
|
}
|
|
}
|
|
|
|
if(!_insertGlitchBgPixel) {
|
|
_bgFifo.Pop();
|
|
}
|
|
_insertGlitchBgPixel = false;
|
|
_drawnPixels++;
|
|
|
|
if(_oamFifo.Size > 0) {
|
|
_oamFifo.Pop();
|
|
}
|
|
}
|
|
|
|
ClockTileFetcher();
|
|
}
|
|
|
|
void GbPpu::WriteBgPixel(uint8_t colorIndex)
|
|
{
|
|
uint16_t outOffset = _state.Scanline * GbConstants::ScreenWidth + _drawnPixels;
|
|
_currentBuffer[outOffset] = LcdReadBgPalette(colorIndex) & 0x7FFF;
|
|
if(_gameboy->IsSgb()) {
|
|
_gameboy->GetSgb()->WriteLcdColor(_state.Scanline, (uint8_t)_drawnPixels, colorIndex & 0x03);
|
|
}
|
|
}
|
|
|
|
void GbPpu::WriteObjPixel(uint8_t colorIndex)
|
|
{
|
|
uint16_t outOffset = _state.Scanline * GbConstants::ScreenWidth + _drawnPixels;
|
|
_currentBuffer[outOffset] = LcdReadObjPalette(colorIndex) & 0x7FFF;
|
|
if(_gameboy->IsSgb()) {
|
|
_gameboy->GetSgb()->WriteLcdColor(_state.Scanline, (uint8_t)_drawnPixels, colorIndex & 0x03);
|
|
}
|
|
}
|
|
|
|
void GbPpu::RunSpriteEvaluation()
|
|
{
|
|
if(_state.Cycle & 0x01) {
|
|
if(_spriteCount < 10 || (_spriteCount < 40 && _settings->GetGameboyConfig().RemoveSpriteLimit)) {
|
|
uint8_t spriteIndex = ((_state.Cycle - 4) >> 1) * 4;
|
|
|
|
if(!_dmaController->IsOamDmaRunning()) {
|
|
//When DMA is running, the LCD appears to re-use the last values read from OAM
|
|
//These are shared with the sprite fetcher, so they can contain tile index/attributes
|
|
//instead of Y+X coordinates, depending on when the OAM DMA was started.
|
|
//This is needed to pass the "strikethrough" test rom
|
|
_oamReadBuffer[0] = _oam[spriteIndex]; //Y
|
|
_oamReadBuffer[1] = _oam[spriteIndex + 1]; //X
|
|
}
|
|
|
|
int16_t sprY = ((int16_t)_oamReadBuffer[0] - 16);
|
|
if(_state.Scanline >= sprY && _state.Scanline < sprY + (_state.LargeSprites ? 16 : 8)) {
|
|
_spriteY[_spriteCount] = _oamReadBuffer[0];
|
|
_spriteX[_spriteCount] = _oamReadBuffer[1];
|
|
_spriteIndexes[_spriteCount] = spriteIndex;
|
|
_spriteCount++;
|
|
}
|
|
}
|
|
} else {
|
|
//TODO check proper timing for even&odd cycles
|
|
}
|
|
}
|
|
|
|
void GbPpu::LoadExtraSprite()
|
|
{
|
|
//Push the sprite into the FIFO immediately without delaying the LCD, etc.
|
|
for(int i = 0; i < 6; i++) {
|
|
ClockSpriteFetcher();
|
|
}
|
|
}
|
|
|
|
void GbPpu::ResetRenderer()
|
|
{
|
|
//Reset fetcher & pixel FIFO
|
|
_oamFifo.Reset();
|
|
_oamFetcher.Step = 0;
|
|
|
|
_bgFifo.Reset();
|
|
_bgFifo.Size = 8;
|
|
_bgFetcher.Step = 0;
|
|
|
|
_drawnPixels = -8 - (_state.ScrollX & 0x07);
|
|
_fetchSprite = -1;
|
|
_fetchWindow = false;
|
|
_fetchColumn = 0;
|
|
|
|
_insertGlitchBgPixel = false;
|
|
}
|
|
|
|
void GbPpu::ClockSpriteFetcher()
|
|
{
|
|
switch(_oamFetcher.Step++) {
|
|
case 1: {
|
|
//Fetch tile index
|
|
uint8_t sprAddr = _spriteIndexes[_fetchSprite];
|
|
int16_t sprY = (int16_t)_spriteY[_fetchSprite] - 16;
|
|
|
|
if(!_dmaController->IsOamDmaRunning()) {
|
|
_oamReadBuffer[0] = LcdReadOam(sprAddr + 2); //Tile Index
|
|
_oamReadBuffer[1] = LcdReadOam(sprAddr + 3); //Attributes
|
|
} else {
|
|
//When DMA is running, this data is replaced by the content
|
|
//at the address that the DMA unit was reading/writing at the time.
|
|
uint16_t addr = _dmaController->GetLastWriteAddress() & 0xFE;
|
|
_oamReadBuffer[0] = _oam[addr];
|
|
_oamReadBuffer[1] = _oam[addr + 1];
|
|
}
|
|
|
|
uint8_t sprTile = _oamReadBuffer[0];
|
|
uint8_t sprAttr = _oamReadBuffer[1];
|
|
bool vMirror = (sprAttr & 0x40) != 0;
|
|
uint16_t tileBank = _state.CgbEnabled ? ((sprAttr & 0x08) ? 0x2000 : 0x0000) : 0;
|
|
|
|
uint8_t sprOffsetY = vMirror ? (_state.LargeSprites ? 15 : 7) - (_state.Scanline - sprY) : (_state.Scanline - sprY);
|
|
if(_state.LargeSprites) {
|
|
sprTile &= 0xFE;
|
|
}
|
|
|
|
uint16_t sprTileAddr = (sprTile * 16 + sprOffsetY * 2) | tileBank;
|
|
_oamFetcher.Addr = sprTileAddr;
|
|
_oamFetcher.Attributes = sprAttr;
|
|
break;
|
|
}
|
|
|
|
case 3: _oamFetcher.LowByte = LcdReadVram(_oamFetcher.Addr); break;
|
|
|
|
case 5: {
|
|
//Fetch sprite data (high byte)
|
|
_oamFetcher.HighByte = LcdReadVram(_oamFetcher.Addr + 1);
|
|
PushSpriteToPixelFifo();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void GbPpu::FindNextSprite()
|
|
{
|
|
if(_fetchSprite < 0 && (_state.SpritesEnabled || _state.CgbEnabled)) {
|
|
for(int i = 0; i < _spriteCount; i++) {
|
|
if((int)_spriteX[i] - 8 == _drawnPixels) {
|
|
_fetchSprite = i;
|
|
_spriteX[i] = 0xFF; //Prevent processing the same sprite again
|
|
_oamFetcher.Step = 0;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void GbPpu::ClockTileFetcher()
|
|
{
|
|
_evtColor = EvtColor::RenderingBgLoad;
|
|
|
|
switch(_bgFetcher.Step++) {
|
|
case 1: {
|
|
//Fetch tile index
|
|
uint16_t tilemapAddr;
|
|
uint8_t yOffset;
|
|
uint8_t scrollPos;
|
|
if(_fetchWindow) {
|
|
tilemapAddr = _state.WindowTilemapSelect ? 0x1C00 : 0x1800;
|
|
yOffset = (uint8_t)_windowCounter;
|
|
scrollPos = _fetchColumn;
|
|
} else {
|
|
tilemapAddr = _state.BgTilemapSelect ? 0x1C00 : 0x1800;
|
|
yOffset = _state.ScrollY + _state.Scanline;
|
|
scrollPos = (_fetchColumn + (_state.ScrollX / 8)) & 0x1F;
|
|
}
|
|
|
|
uint8_t row = yOffset >> 3;
|
|
uint16_t tileAddr = tilemapAddr + scrollPos + row * 32;
|
|
_tileIndex = LcdReadVram(tileAddr);
|
|
|
|
uint8_t attributes = _state.CgbEnabled ? _vram[tileAddr | 0x2000] : 0;
|
|
bool vMirror = (attributes & 0x40) != 0;
|
|
uint16_t tileBank = (attributes & 0x08) ? 0x2000 : 0x0000;
|
|
|
|
uint8_t tileY = vMirror ? (7 - (yOffset & 0x07)) : (yOffset & 0x07);
|
|
|
|
uint16_t tileRowAddr = tileY * 2;
|
|
if(_state.BgTileSelect) {
|
|
tileRowAddr += _tileIndex * 16;
|
|
} else {
|
|
tileRowAddr += 0x1000 + (int8_t)_tileIndex * 16;
|
|
}
|
|
|
|
tileRowAddr |= tileBank;
|
|
_bgFetcher.Addr = tileRowAddr;
|
|
_bgFetcher.Attributes = (attributes & 0xBF);
|
|
break;
|
|
}
|
|
|
|
case 3: {
|
|
//Fetch tile data (low byte)
|
|
if(_gbcTileGlitch) {
|
|
_bgFetcher.LowByte = _tileIndex;
|
|
} else {
|
|
_bgFetcher.LowByte = LcdReadVram(_bgFetcher.Addr);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 5: {
|
|
//Fetch tile data (high byte)
|
|
if(_gbcTileGlitch) {
|
|
_bgFetcher.HighByte = _tileIndex;
|
|
} else {
|
|
_bgFetcher.HighByte = LcdReadVram(_bgFetcher.Addr + 1);
|
|
}
|
|
|
|
[[fallthrough]];
|
|
}
|
|
|
|
case 6:
|
|
case 7:
|
|
if(_bgFifo.Size == 0) {
|
|
PushTileToPixelFifo();
|
|
} else if(_bgFetcher.Step == 8) {
|
|
//Wait until fifo is empty to push pixels
|
|
_bgFetcher.Step = 7;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
void GbPpu::PushSpriteToPixelFifo()
|
|
{
|
|
uint8_t spriteIndex = _fetchSprite;
|
|
|
|
_fetchSprite = -1;
|
|
_oamFetcher.Step = 0;
|
|
|
|
if(!_state.SpritesEnabled) {
|
|
return;
|
|
}
|
|
|
|
uint8_t pos = _oamFifo.Position;
|
|
|
|
//Overlap sprite
|
|
for(int i = 0; i < 8; i++) {
|
|
uint8_t shift = (_oamFetcher.Attributes & 0x20) ? i : (7 - i);
|
|
uint8_t bits = ((_oamFetcher.LowByte >> shift) & 0x01);
|
|
bits |= ((_oamFetcher.HighByte >> shift) & 0x01) << 1;
|
|
|
|
if(bits > 0 && (_oamFifo.Content[pos].Color == 0 || (IsCgbEnabled() && spriteIndex <= _oamFifo.Content[pos].Index))) {
|
|
_oamFifo.Content[pos].Color = bits;
|
|
_oamFifo.Content[pos].Attributes = _oamFetcher.Attributes;
|
|
_oamFifo.Content[pos].Index = spriteIndex;
|
|
}
|
|
pos = (pos + 1) & 0x07;
|
|
}
|
|
_oamFifo.Size = 8;
|
|
}
|
|
|
|
void GbPpu::PushTileToPixelFifo()
|
|
{
|
|
//Add new tile to fifo
|
|
for(int i = 0; i < 8; i++) {
|
|
uint8_t shift = (_bgFetcher.Attributes & 0x20) ? i : (7 - i);
|
|
uint8_t bits = ((_bgFetcher.LowByte >> shift) & 0x01);
|
|
bits |= ((_bgFetcher.HighByte >> shift) & 0x01) << 1;
|
|
|
|
_bgFifo.Content[i].Color = bits;
|
|
_bgFifo.Content[i].Attributes = _bgFetcher.Attributes;
|
|
}
|
|
|
|
_fetchColumn = (_fetchColumn + 1) & 0x1F;
|
|
_bgFifo.Position = 0;
|
|
_bgFifo.Size = 8;
|
|
_bgFetcher.Step = 0;
|
|
}
|
|
|
|
void GbPpu::UpdateStatIrq()
|
|
{
|
|
if(_state.LcdEnabled) {
|
|
bool irqFlag =
|
|
(_state.LyCoincidenceFlag && (_state.Status & GbPpuStatusFlags::CoincidenceIrq)) ||
|
|
(_state.IrqMode == PpuMode::HBlank && (_state.Status & GbPpuStatusFlags::HBlankIrq)) ||
|
|
(_state.IrqMode == PpuMode::OamEvaluation && (_state.Status & GbPpuStatusFlags::OamIrq)) ||
|
|
(_state.IrqMode == PpuMode::VBlank && (_state.Status & GbPpuStatusFlags::VBlankIrq));
|
|
|
|
if(irqFlag && !_state.StatIrqFlag) {
|
|
_memoryManager->RequestIrq(GbIrqSource::LcdStat);
|
|
}
|
|
_state.StatIrqFlag = irqFlag;
|
|
} else {
|
|
_state.StatIrqFlag = false;
|
|
}
|
|
}
|
|
|
|
uint8_t GbPpu::LcdReadOam(uint8_t addr)
|
|
{
|
|
return _stopOamBlocked ? 0xFF : _oam[addr];
|
|
}
|
|
|
|
uint8_t GbPpu::LcdReadVram(uint16_t addr)
|
|
{
|
|
return _stopVramBlocked ? 0xFF : _vram[addr];
|
|
}
|
|
|
|
uint16_t GbPpu::LcdReadBgPalette(uint8_t addr)
|
|
{
|
|
return _stopPaletteBlocked ? 0 : _state.CgbBgPalettes[addr];
|
|
}
|
|
|
|
uint16_t GbPpu::LcdReadObjPalette(uint8_t addr)
|
|
{
|
|
return _stopPaletteBlocked ? 0 : _state.CgbObjPalettes[addr];
|
|
}
|
|
|
|
uint32_t GbPpu::GetFrameCount()
|
|
{
|
|
return _state.FrameCount;
|
|
}
|
|
|
|
uint8_t GbPpu::GetScanline()
|
|
{
|
|
return _state.Scanline;
|
|
}
|
|
|
|
uint16_t GbPpu::GetCycle()
|
|
{
|
|
return _state.Cycle;
|
|
}
|
|
|
|
bool GbPpu::IsLcdEnabled()
|
|
{
|
|
return _state.LcdEnabled;
|
|
}
|
|
|
|
bool GbPpu::IsCgbEnabled()
|
|
{
|
|
return _state.CgbEnabled;
|
|
}
|
|
|
|
PpuMode GbPpu::GetMode()
|
|
{
|
|
return _state.Mode;
|
|
}
|
|
|
|
void GbPpu::SendFrame()
|
|
{
|
|
_emu->ProcessEvent(EventType::EndFrame, CpuType::Gameboy);
|
|
_state.FrameCount++;
|
|
|
|
if(_gameboy->IsSgb()) {
|
|
_isFirstFrame = false;
|
|
_forceBlankFrame = false;
|
|
return;
|
|
}
|
|
|
|
UpdatePalette();
|
|
|
|
if(_gameboy->IsPrimaryConsole()) {
|
|
_emu->GetNotificationManager()->SendNotification(ConsoleNotificationType::PpuFrameDone);
|
|
}
|
|
|
|
if(_forceBlankFrame) {
|
|
//Send blank frame on the first frame after enabling LCD
|
|
//On CGB some games flicker if this is done when the LCD is only off for a short time (e.g Men in Black - The Series)
|
|
//So for CGB, the screen is only cleared if the LCD has been turned off for a while
|
|
std::fill(_outputBuffers[0], _outputBuffers[0] + GbConstants::PixelCount, 0x7FFF);
|
|
std::fill(_outputBuffers[1], _outputBuffers[1] + GbConstants::PixelCount, 0x7FFF);
|
|
}
|
|
_forceBlankFrame = false;
|
|
_isFirstFrame = false;
|
|
|
|
RenderedFrame frame(_currentBuffer, GbConstants::ScreenWidth, GbConstants::ScreenHeight, 1.0, _state.FrameCount, _gameboy->GetControlManager()->GetPortStates());
|
|
if(_gameboy->GetLinkedConsole()) {
|
|
SendLinkedFrame(frame);
|
|
if(_gameboy->IsPrimaryConsole()) {
|
|
_emu->ProcessEndOfFrame();
|
|
}
|
|
} else {
|
|
bool rewinding = _emu->GetRewindManager()->IsRewinding();
|
|
_emu->GetVideoDecoder()->UpdateFrame(frame, rewinding, rewinding);
|
|
_emu->ProcessEndOfFrame();
|
|
}
|
|
|
|
_gameboy->ProcessEndOfFrame();
|
|
|
|
_currentBuffer = _currentBuffer == _outputBuffers[0] ? _outputBuffers[1] : _outputBuffers[0];
|
|
}
|
|
|
|
void GbPpu::SendLinkedFrame(RenderedFrame& frame)
|
|
{
|
|
GameboyConfig& cfg = _settings->GetGameboyConfig();
|
|
bool forRewind = _emu->GetRewindManager()->IsRewinding();
|
|
|
|
if(cfg.LocalLinkCableVideoOutput == GbLocalLinkOutputOption::MainSystemOnly && _gameboy->IsPrimaryConsole()) {
|
|
_emu->GetVideoDecoder()->UpdateFrame(frame, forRewind, forRewind);
|
|
} else if(cfg.LocalLinkCableVideoOutput == GbLocalLinkOutputOption::SubSystemOnly && !_gameboy->IsPrimaryConsole()) {
|
|
_emu->GetVideoDecoder()->UpdateFrame(frame, forRewind, forRewind);
|
|
} else if(cfg.LocalLinkCableVideoOutput == GbLocalLinkOutputOption::Both) {
|
|
if(_gameboy->IsPrimaryConsole()) {
|
|
GbPpu* otherPpu = _gameboy->GetLinkedConsole()->GetPpu();
|
|
uint16_t* otherFrame;
|
|
if(otherPpu->GetScanline() >= GetScanline() || (otherPpu->GetMode() == PpuMode::HBlank && otherPpu->GetScanline() == GetScanline() - 1)) {
|
|
//When both PPUs are in vblank (or at least, done drawing the current frame), use the latest frame
|
|
otherFrame = otherPpu->GetOutputBuffer();
|
|
} else {
|
|
//When the other PPU is not in vblank, use the previous frame's output to avoid screen tearing
|
|
otherFrame = otherPpu->_currentBuffer == otherPpu->_outputBuffers[0] ? otherPpu->_outputBuffers[1] : otherPpu->_outputBuffers[0];
|
|
}
|
|
|
|
VideoMergeResult merged = AvMergeUtilities::MergeFrames(frame, otherFrame);
|
|
_emu->GetVideoDecoder()->UpdateFrame(merged.Frame, true, forRewind);
|
|
}
|
|
}
|
|
}
|
|
|
|
void GbPpu::DebugSendFrame()
|
|
{
|
|
if(_gameboy->IsSgb()) {
|
|
return;
|
|
}
|
|
|
|
int lastPixel = std::max(0, _state.IrqMode == PpuMode::HBlank ? 160 : _drawnPixels);
|
|
int offset = std::max(0, (int)(lastPixel + 1 + _state.Scanline * GbConstants::ScreenWidth));
|
|
int pixelsToClear = GbConstants::PixelCount - offset;
|
|
if(pixelsToClear > 0) {
|
|
memset(_currentBuffer + offset, 0, pixelsToClear * sizeof(uint16_t));
|
|
}
|
|
|
|
RenderedFrame frame(_currentBuffer, GbConstants::ScreenWidth, GbConstants::ScreenHeight, 1.0, _state.FrameCount);
|
|
|
|
_emu->GetVideoDecoder()->UpdateFrame(frame, false, false);
|
|
//Send twice to prevent LCD blending behavior
|
|
_emu->GetVideoDecoder()->UpdateFrame(frame, false, false);
|
|
}
|
|
|
|
void GbPpu::UpdatePalette()
|
|
{
|
|
if(!_gameboy->IsCgb()) {
|
|
GameboyConfig cfg = _emu->GetSettings()->GetGameboyConfig();
|
|
for(int i = 0; i < 4; i++) {
|
|
//Set palette based on settings (DMG)
|
|
uint16_t bgColor = ((cfg.BgColors[i] & 0xF8) << 7) | ((cfg.BgColors[i] & 0xF800) >> 6) | ((cfg.BgColors[i] & 0xF80000) >> 19);
|
|
_state.CgbBgPalettes[i] = bgColor;
|
|
|
|
uint16_t obj0Color = ((cfg.Obj0Colors[i] & 0xF8) << 7) | ((cfg.Obj0Colors[i] & 0xF800) >> 6) | ((cfg.Obj0Colors[i] & 0xF80000) >> 19);
|
|
_state.CgbObjPalettes[i] = obj0Color;
|
|
|
|
uint16_t obj1Color = ((cfg.Obj1Colors[i] & 0xF8) << 7) | ((cfg.Obj1Colors[i] & 0xF800) >> 6) | ((cfg.Obj1Colors[i] & 0xF80000) >> 19);
|
|
_state.CgbObjPalettes[i + 4] = obj1Color;
|
|
}
|
|
}
|
|
}
|
|
|
|
void GbPpu::SetMode(PpuMode mode)
|
|
{
|
|
_state.Mode = mode;
|
|
_dmaController->ProcessHdma();
|
|
}
|
|
|
|
uint8_t GbPpu::Read(uint16_t addr)
|
|
{
|
|
switch(addr) {
|
|
case 0xFF40: return _state.Control;
|
|
case 0xFF41:
|
|
//FF41 - STAT - LCDC Status (R/W)
|
|
return (
|
|
0x80 |
|
|
(_state.Status & 0x78) |
|
|
(_state.LyCoincidenceFlag ? 0x04 : 0x00) |
|
|
(int)_state.Mode);
|
|
|
|
case 0xFF42: return _state.ScrollY; //FF42 - SCY - Scroll Y (R/W)
|
|
case 0xFF43: return _state.ScrollX; //FF43 - SCX - Scroll X (R/W)
|
|
case 0xFF44: return _state.Ly; //FF44 - LY - LCDC Y-Coordinate (R)
|
|
case 0xFF45: return _state.LyCompare; //FF45 - LYC - LY Compare (R/W)
|
|
case 0xFF47: return _state.BgPalette; //FF47 - BGP - BG Palette Data (R/W) - Non CGB Mode Only
|
|
case 0xFF48: return _state.ObjPalette0; //FF48 - OBP0 - Object Palette 0 Data (R/W) - Non CGB Mode Only
|
|
case 0xFF49: return _state.ObjPalette1; //FF49 - OBP1 - Object Palette 1 Data (R/W) - Non CGB Mode Only
|
|
case 0xFF4A: return _state.WindowY; //FF4A - WY - Window Y Position (R/W)
|
|
case 0xFF4B: return _state.WindowX; //FF4B - WX - Window X Position minus 7 (R/W)
|
|
}
|
|
|
|
LogDebug("[Debug] GB - Missing read handler: $" + HexUtilities::ToHex(addr));
|
|
return 0xFF;
|
|
}
|
|
|
|
void GbPpu::Write(uint16_t addr, uint8_t value)
|
|
{
|
|
switch(addr) {
|
|
case 0xFF40:
|
|
_state.Control = value;
|
|
if(_state.LcdEnabled != ((value & 0x80) != 0)) {
|
|
_state.LcdEnabled = (value & 0x80) != 0;
|
|
|
|
if(!_state.LcdEnabled) {
|
|
//Reset LCD to top of screen when it gets turned off
|
|
if(_state.Mode != PpuMode::VBlank) {
|
|
_emu->BreakIfDebugging(CpuType::Gameboy, BreakSource::GbDisableLcdOutsideVblank);
|
|
SendFrame();
|
|
}
|
|
|
|
_state.Cycle = 0;
|
|
_state.Scanline = 0;
|
|
_state.Ly = 0;
|
|
_state.LyForCompare = 0;
|
|
|
|
_oamReadBlocked = false;
|
|
_oamWriteBlocked = false;
|
|
_vramReadBlocked = false;
|
|
_vramWriteBlocked = false;
|
|
|
|
_state.IrqMode = PpuMode::NoIrq;
|
|
_wyEnableFlag = false;
|
|
_lcdDisabled = true;
|
|
|
|
_lastFrameTime = _gameboy->GetApuCycleCount();
|
|
|
|
SetMode(PpuMode::HBlank);
|
|
} else {
|
|
_lcdDisabled = false;
|
|
_isFirstFrame = true;
|
|
_forceBlankFrame = !_gameboy->IsCgb() || (_gameboy->GetApuCycleCount() - _lastFrameTime) > 5000;
|
|
_state.Cycle = 7;
|
|
_state.IdleCycles = 0;
|
|
_state.IrqMode = PpuMode::NoIrq;
|
|
ResetRenderer();
|
|
_state.LyCoincidenceFlag = _state.LyCompare == _state.LyForCompare;
|
|
UpdateStatIrq();
|
|
|
|
if(_emu->IsDebugging()) {
|
|
_emu->ProcessEvent(EventType::StartFrame, CpuType::Gameboy);
|
|
|
|
_currentEventViewerBuffer = _currentEventViewerBuffer == _eventViewerBuffers[0] ? _eventViewerBuffers[1] : _eventViewerBuffers[0];
|
|
std::fill(_currentEventViewerBuffer, _currentEventViewerBuffer + GbConstants::EventViewerBufferSize, evtColors[0]);
|
|
}
|
|
}
|
|
}
|
|
_state.WindowTilemapSelect = (value & 0x40) != 0;
|
|
_state.WindowEnabled = (value & 0x20) != 0;
|
|
_state.BgTileSelect = (value & 0x10) != 0;
|
|
_state.BgTilemapSelect = (value & 0x08) != 0;
|
|
_state.LargeSprites = (value & 0x04) != 0;
|
|
_state.SpritesEnabled = (value & 0x02) != 0;
|
|
_state.BgEnabled = (value & 0x01) != 0;
|
|
|
|
_wyEnableFlag |= _state.Scanline == _state.WindowY && _state.WindowEnabled;
|
|
break;
|
|
|
|
case 0xFF41:
|
|
if(!_gameboy->IsCgb()) {
|
|
//STAT write bug (DMG ONLY)
|
|
//Writing to STAT causes all IRQ types to be turned on for a single cycle
|
|
_state.Status = 0xF8 | (_state.Status & 0x07);
|
|
UpdateStatIrq();
|
|
}
|
|
|
|
_state.Status = value & 0xF8;
|
|
UpdateStatIrq();
|
|
break;
|
|
|
|
case 0xFF42: _state.ScrollY = value; break;
|
|
case 0xFF43: _state.ScrollX = value; break;
|
|
case 0xFF45:
|
|
_state.LyCompare = value;
|
|
if(_state.LcdEnabled) {
|
|
_state.IdleCycles = 0;
|
|
_state.LyCoincidenceFlag = (_state.LyCompare == _state.LyForCompare);
|
|
UpdateStatIrq();
|
|
}
|
|
break;
|
|
|
|
case 0xFF47:
|
|
if(!_state.CgbEnabled && _state.Mode == PpuMode::Drawing && _drawnPixels > 0 && _lastPixelType == GbPixelType::Background) {
|
|
//When BGP is changed during rendering, the current pixel is affected.
|
|
//Re-draw the last pixel with the correct color.
|
|
|
|
//On CGB, the pixel uses the new BGP value only
|
|
uint8_t bgpValue = value;
|
|
if(!_gameboy->IsCgb()) {
|
|
//On DMG, the pixel uses the new BGP value ORed with the old value
|
|
bgpValue |= _state.BgPalette;
|
|
}
|
|
|
|
_drawnPixels--;
|
|
WriteBgPixel((bgpValue >> (_lastBgColor * 2)) & 0x03);
|
|
if(_emu->IsDebugging()) {
|
|
//Update the event viewer data, if the debugger is running
|
|
_currentEventViewerBuffer[456 * _state.Scanline + _state.Cycle] = _currentBuffer[_state.Scanline * GbConstants::ScreenWidth + _drawnPixels];
|
|
}
|
|
_drawnPixels++;
|
|
}
|
|
_state.BgPalette = value;
|
|
break;
|
|
|
|
case 0xFF48: _state.ObjPalette0 = value; break;
|
|
case 0xFF49: _state.ObjPalette1 = value; break;
|
|
case 0xFF4A: _state.WindowY = value; break;
|
|
case 0xFF4B: _state.WindowX = value; break;
|
|
|
|
default:
|
|
LogDebug("[Debug] GB - Missing write handler: $" + HexUtilities::ToHex(addr));
|
|
break;
|
|
}
|
|
}
|
|
|
|
void GbPpu::SetTileFetchGlitchState()
|
|
{
|
|
_gbcTileGlitch = true;
|
|
}
|
|
|
|
bool GbPpu::IsVramReadAllowed()
|
|
{
|
|
return !_vramReadBlocked;
|
|
}
|
|
|
|
bool GbPpu::IsVramWriteAllowed()
|
|
{
|
|
return !_vramWriteBlocked;
|
|
}
|
|
|
|
uint8_t GbPpu::ReadVram(uint16_t addr)
|
|
{
|
|
if(IsVramReadAllowed()) {
|
|
uint16_t vramAddr = (_state.CgbVramBank << 13) | (addr & 0x1FFF);
|
|
_emu->ProcessPpuRead<CpuType::Gameboy>(vramAddr, _vram[vramAddr], MemoryType::GbVideoRam);
|
|
return _vram[vramAddr];
|
|
} else {
|
|
_emu->BreakIfDebugging(CpuType::Gameboy, BreakSource::GbInvalidVramAccess);
|
|
return 0xFF;
|
|
}
|
|
}
|
|
|
|
uint8_t GbPpu::PeekVram(uint16_t addr)
|
|
{
|
|
return IsVramReadAllowed() ? _vram[(_state.CgbVramBank << 13) | (addr & 0x1FFF)] : 0xFF;
|
|
}
|
|
|
|
void GbPpu::WriteVram(uint16_t addr, uint8_t value)
|
|
{
|
|
if(IsVramWriteAllowed()) {
|
|
uint16_t vramAddr = (_state.CgbVramBank << 13) | (addr & 0x1FFF);
|
|
_emu->ProcessPpuWrite<CpuType::Gameboy>(vramAddr, value, MemoryType::GbVideoRam);
|
|
_vram[vramAddr] = value;
|
|
} else {
|
|
_emu->BreakIfDebugging(CpuType::Gameboy, BreakSource::GbInvalidVramAccess);
|
|
}
|
|
}
|
|
|
|
bool GbPpu::IsOamWriteAllowed()
|
|
{
|
|
return !_oamWriteBlocked && !_memoryManager->IsOamDmaRunning();
|
|
}
|
|
|
|
bool GbPpu::IsOamReadAllowed()
|
|
{
|
|
return !_oamReadBlocked && !_memoryManager->IsOamDmaRunning();
|
|
}
|
|
|
|
uint8_t GbPpu::PeekOam(uint8_t addr)
|
|
{
|
|
if(addr < 0xA0) {
|
|
return IsOamReadAllowed() ? _oam[addr] : 0xFF;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
uint8_t GbPpu::ReadOam(uint8_t addr)
|
|
{
|
|
if(addr < 0xA0) {
|
|
if(IsOamReadAllowed()) {
|
|
_emu->ProcessPpuRead<CpuType::Gameboy>(addr, _oam[addr], MemoryType::GbSpriteRam);
|
|
return _oam[addr];
|
|
} else {
|
|
_emu->BreakIfDebugging(CpuType::Gameboy, BreakSource::GbInvalidOamAccess);
|
|
return 0xFF;
|
|
}
|
|
}
|
|
|
|
//"This area returns $FF when OAM is blocked, and otherwise the behavior depends on the hardware revision."
|
|
//TODOGB CGB behavior
|
|
return _memoryManager->IsOamDmaRunning() ? 0xFF : 0;
|
|
}
|
|
|
|
void GbPpu::WriteOam(uint8_t addr, uint8_t value, bool forDma)
|
|
{
|
|
//During DMA or rendering/oam evaluation, ignore writes to OAM
|
|
//The DMA controller is always allowed to write to OAM (presumably the PPU can't read OAM during that time? TODO implement)
|
|
//On the DMG, there is a 4 clock gap (80 to 83) between OAM evaluation & rendering where writing is allowed
|
|
if(addr < 0xA0) {
|
|
if(forDma) {
|
|
_emu->ProcessPpuWrite<CpuType::Gameboy>(addr, value, MemoryType::GbSpriteRam);
|
|
_oam[addr] = value;
|
|
} else if(IsOamWriteAllowed()) {
|
|
_emu->ProcessPpuWrite<CpuType::Gameboy>(addr, value, MemoryType::GbSpriteRam);
|
|
_oam[addr] = value;
|
|
} else {
|
|
_emu->BreakIfDebugging(CpuType::Gameboy, BreakSource::GbInvalidOamAccess);
|
|
}
|
|
}
|
|
}
|
|
|
|
template<GbOamCorruptionType oamCorruptionType>
|
|
void GbPpu::ProcessOamCorruption(uint16_t addr)
|
|
{
|
|
if(_gameboy->IsCgb() || _state.Cycle >= 83 || _state.Mode != PpuMode::OamEvaluation || (addr & 0xFF00) != 0xFE00) {
|
|
return;
|
|
}
|
|
|
|
int row = std::min(19, (_state.Cycle - 2) >> 2);
|
|
|
|
//Quotes from: https://gbdev.io/pandocs/OAM_Corruption_Bug.html
|
|
//"Sprites 1 & 2 ($FE00 & $FE04) are not affected by this bug"
|
|
if(row == 0) {
|
|
return;
|
|
}
|
|
|
|
//"If a register is increased or decreased in the same M cycle of a write, this will effectively trigger both
|
|
//a read and a write in a single M-cycle, resulting in a more complex corruption pattern:"
|
|
if constexpr(oamCorruptionType == GbOamCorruptionType::ReadIncDec) {
|
|
ProcessOamIncDecCorruption(row);
|
|
//"Regardless of whether the previous corruption occurred or not, a normal read corruption is then applied."
|
|
}
|
|
|
|
int prevRow = row - 1;
|
|
uint16_t a = _oam[row * 8] | (_oam[row * 8 + 1] << 8);
|
|
uint16_t b = _oam[prevRow * 8] | (_oam[prevRow * 8 + 1] << 8);
|
|
uint16_t c = _oam[prevRow * 8 + 4] | (_oam[prevRow * 8 + 5] << 8);
|
|
|
|
uint16_t result;
|
|
if constexpr(oamCorruptionType == GbOamCorruptionType::Write) {
|
|
//"The first word in the row is replaced with this bitwise expression: ((a ^ c) & (b ^ c)) ^ c,
|
|
//where a is the original value of that word, b is the first word in the preceding row, and c
|
|
//is the third word in the preceding row.
|
|
result = ((a ^ c) & (b ^ c)) ^ c;
|
|
} else {
|
|
//"A "read corruption" works similarly to a write corruption, except the bitwise expression is b | (a & c)."
|
|
result = b | (a & c);
|
|
}
|
|
|
|
_oam[row * 8] = (uint8_t)result;
|
|
_oam[row * 8 + 1] = result >> 8;
|
|
|
|
//"The last three words are copied from the last three words in the preceding row."
|
|
memcpy(_oam + row * 8 + 2, _oam + prevRow * 8 + 2, 3 * sizeof(uint16_t));
|
|
}
|
|
|
|
void GbPpu::ProcessOamIncDecCorruption(int row)
|
|
{
|
|
//"This corruption will not happen if the accessed row is one of the first four, as well as if it's the last row"
|
|
if(row >= 4 && row < 19) {
|
|
int prevRow = row - 1;
|
|
//"The first word in the row preceding the currently accessed row is replaced with the following bitwise expression:
|
|
//(b & (a | c | d)) | (a & c & d) where a is the first word two rows before the currently accessed row, b is the first
|
|
//word in the preceding row (the word being corrupted), c is the first word in the currently accessed row, and d is
|
|
//the third word in the preceding row."
|
|
uint16_t a = _oam[(row - 2) * 8] | (_oam[(row - 2) * 8 + 1] << 8);
|
|
uint16_t b = _oam[prevRow * 8] | (_oam[prevRow * 8 + 1] << 8);
|
|
uint16_t c = _oam[row * 8] | (_oam[row * 8 + 1] << 8);
|
|
uint16_t d = _oam[prevRow * 8 + 4] | (_oam[prevRow * 8 + 5] << 8);
|
|
|
|
uint16_t result = (b & (a | c | d)) | (a & c & d);
|
|
_oam[prevRow * 8] = (uint8_t)result;
|
|
_oam[prevRow * 8 + 1] = result >> 8;
|
|
|
|
//"The contents of the preceding row is copied (after the corruption of the first word in it) both to the
|
|
//currently accessed row and to two rows before the currently accessed row"
|
|
memcpy(_oam + row * 8, _oam + prevRow * 8, 4 * sizeof(uint16_t));
|
|
memcpy(_oam + (row - 2) * 8, _oam + prevRow * 8, 4 * sizeof(uint16_t));
|
|
}
|
|
}
|
|
|
|
uint8_t GbPpu::ReadCgbRegister(uint16_t addr)
|
|
{
|
|
//TODOGBC restrict read/write access to GBC palette during rendering
|
|
if(!_state.CgbEnabled) {
|
|
return 0xFF;
|
|
}
|
|
|
|
switch(addr) {
|
|
case 0xFF4F: return _state.CgbVramBank | 0xFE;
|
|
case 0xFF68: return _state.CgbBgPalPosition | (_state.CgbBgPalAutoInc ? 0x80 : 0) | 0x40;
|
|
case 0xFF69: return ReadCgbPalette(_state.CgbBgPalPosition, _state.CgbBgPalettes);
|
|
case 0xFF6A: return _state.CgbObjPalPosition | (_state.CgbObjPalAutoInc ? 0x80 : 0) | 0x40;
|
|
case 0xFF6B: return ReadCgbPalette(_state.CgbObjPalPosition, _state.CgbObjPalettes);
|
|
}
|
|
LogDebug("[Debug] GBC - Missing read handler: $" + HexUtilities::ToHex(addr));
|
|
return 0xFF;
|
|
}
|
|
|
|
void GbPpu::WriteCgbRegister(uint16_t addr, uint8_t value)
|
|
{
|
|
if(!_state.CgbEnabled && _memoryManager->IsBootRomDisabled()) {
|
|
return;
|
|
}
|
|
|
|
switch(addr) {
|
|
case 0xFF4C: _state.CgbEnabled = (value & 0x0C) == 0; break;
|
|
case 0xFF4F: _state.CgbVramBank = value & 0x01; break;
|
|
|
|
case 0xFF68:
|
|
//FF68 - BCPS/BGPI - CGB Mode Only - Background Palette Index
|
|
_state.CgbBgPalPosition = value & 0x3F;
|
|
_state.CgbBgPalAutoInc = (value & 0x80) != 0;
|
|
break;
|
|
|
|
case 0xFF69: {
|
|
//FF69 - BCPD/BGPD - CGB Mode Only - Background Palette Data
|
|
WriteCgbPalette(_state.CgbBgPalPosition, _state.CgbBgPalettes, _state.CgbBgPalAutoInc, value);
|
|
break;
|
|
}
|
|
|
|
case 0xFF6A:
|
|
//FF6A - OCPS/OBPI - CGB Mode Only - Sprite Palette Index
|
|
_state.CgbObjPalPosition = value & 0x3F;
|
|
_state.CgbObjPalAutoInc = (value & 0x80) != 0;
|
|
break;
|
|
|
|
case 0xFF6B:
|
|
//FF6B - OCPD/OBPD - CGB Mode Only - Sprite Palette Data
|
|
WriteCgbPalette(_state.CgbObjPalPosition, _state.CgbObjPalettes, _state.CgbObjPalAutoInc, value);
|
|
break;
|
|
|
|
default:
|
|
LogDebug("[Debug] GBC - Missing write handler: $" + HexUtilities::ToHex(addr));
|
|
break;
|
|
}
|
|
}
|
|
|
|
uint8_t GbPpu::ReadCgbPalette(uint8_t& pos, uint16_t* pal)
|
|
{
|
|
if(_state.Mode <= PpuMode::OamEvaluation) {
|
|
uint8_t value = (pal[pos >> 1] >> ((pos & 0x01) ? 8 : 0)) & 0xFF;
|
|
_emu->ProcessPpuRead<CpuType::Gameboy>(pos, value, (pal == _state.CgbBgPalettes) ? MemoryType::GbBgPaletteRam : MemoryType::GbObjPaletteRam);
|
|
return value;
|
|
}
|
|
return 0xFF;
|
|
}
|
|
|
|
void GbPpu::WriteCgbPalette(uint8_t& pos, uint16_t* pal, bool autoInc, uint8_t value)
|
|
{
|
|
if(_state.Mode <= PpuMode::OamEvaluation) {
|
|
_emu->ProcessPpuWrite<CpuType::Gameboy>(pos, value, (pal == _state.CgbBgPalettes) ? MemoryType::GbBgPaletteRam : MemoryType::GbObjPaletteRam);
|
|
if(pos & 0x01) {
|
|
pal[pos >> 1] = (pal[pos >> 1] & 0xFF) | (value << 8);
|
|
} else {
|
|
pal[pos >> 1] = (pal[pos >> 1] & 0xFF00) | value;
|
|
}
|
|
}
|
|
|
|
if(autoInc) {
|
|
pos = (pos + 1) & 0x3F;
|
|
}
|
|
}
|
|
|
|
void GbPpu::Serialize(Serializer& s)
|
|
{
|
|
SV(_state.Scanline);
|
|
SV(_state.Cycle);
|
|
SV(_state.Mode);
|
|
SV(_state.LyCompare);
|
|
SV(_state.BgPalette);
|
|
SV(_state.ObjPalette0);
|
|
SV(_state.ObjPalette1);
|
|
SV(_state.ScrollX);
|
|
SV(_state.ScrollY);
|
|
SV(_state.WindowX);
|
|
SV(_state.WindowY);
|
|
SV(_state.Control);
|
|
SV(_state.LcdEnabled);
|
|
SV(_state.WindowTilemapSelect);
|
|
SV(_state.WindowEnabled);
|
|
SV(_state.BgTileSelect);
|
|
SV(_state.BgTilemapSelect);
|
|
SV(_state.LargeSprites);
|
|
SV(_state.SpritesEnabled);
|
|
SV(_state.BgEnabled);
|
|
SV(_state.Status);
|
|
SV(_state.FrameCount);
|
|
SV(_state.LyCoincidenceFlag);
|
|
SV(_state.CgbBgPalAutoInc);
|
|
SV(_state.CgbBgPalPosition);
|
|
SV(_state.CgbObjPalAutoInc);
|
|
SV(_state.CgbObjPalPosition);
|
|
SV(_state.CgbVramBank);
|
|
SV(_state.CgbEnabled);
|
|
SV(_state.Ly);
|
|
SV(_state.StatIrqFlag);
|
|
|
|
if(_gameboy->IsCgb()) {
|
|
//Only save the palettes for GBC states
|
|
//This makes the process of loading a GB state onto a GBC
|
|
//instance better since the GBC colors will be preserved
|
|
SVArray(_state.CgbBgPalettes, 4 * 8);
|
|
SVArray(_state.CgbObjPalettes, 4 * 8);
|
|
}
|
|
|
|
if(s.GetFormat() != SerializeFormat::Map) {
|
|
//Hide these entries from the Lua API
|
|
SV(_windowCounter);
|
|
SV(_isFirstFrame);
|
|
SV(_rendererIdle);
|
|
SV(_forceBlankFrame);
|
|
SV(_wyEnableFlag);
|
|
SV(_wxEnableFlag);
|
|
SV(_state.IdleCycles);
|
|
SV(_lastFrameTime);
|
|
|
|
SV(_state.LyForCompare);
|
|
SV(_state.IrqMode);
|
|
|
|
SV(_oamReadBlocked);
|
|
SV(_oamWriteBlocked);
|
|
SV(_vramReadBlocked);
|
|
SV(_vramWriteBlocked);
|
|
|
|
SV(_lastPixelType);
|
|
SV(_lastBgColor);
|
|
SV(_insertGlitchBgPixel);
|
|
|
|
SV(_gbcTileGlitch);
|
|
SV(_tileIndex);
|
|
|
|
SV(_stopOamBlocked);
|
|
SV(_stopVramBlocked);
|
|
SV(_stopPaletteBlocked);
|
|
SV(_lcdDisabled);
|
|
|
|
SV(_bgFetcher.Attributes);
|
|
SV(_bgFetcher.Step);
|
|
SV(_bgFetcher.Addr);
|
|
SV(_bgFetcher.LowByte);
|
|
SV(_bgFetcher.HighByte);
|
|
SV(_oamFetcher.Attributes);
|
|
SV(_oamFetcher.Step);
|
|
SV(_oamFetcher.Addr);
|
|
SV(_oamFetcher.LowByte);
|
|
SV(_oamFetcher.HighByte);
|
|
SV(_drawnPixels);
|
|
SV(_fetchColumn);
|
|
SV(_fetchWindow);
|
|
SV(_fetchSprite);
|
|
SV(_spriteCount);
|
|
SV(_bgFifo.Position);
|
|
SV(_bgFifo.Size);
|
|
SV(_oamFifo.Position);
|
|
SV(_oamFifo.Size);
|
|
|
|
for(int i = 0; i < 8; i++) {
|
|
SVI(_bgFifo.Content[i].Color);
|
|
SVI(_bgFifo.Content[i].Attributes);
|
|
SVI(_oamFifo.Content[i].Color);
|
|
SVI(_oamFifo.Content[i].Attributes);
|
|
}
|
|
|
|
SVArray(_oamReadBuffer, 2);
|
|
SVArray(_spriteX, 40);
|
|
SVArray(_spriteY, 40);
|
|
SVArray(_spriteIndexes, 40);
|
|
|
|
SV(_overclockScanlineCount);
|
|
SV(_vblankStartScanline);
|
|
SV(_lastScanline);
|
|
}
|
|
|
|
if(!s.IsSaving()) {
|
|
//For save state compatibility
|
|
if(!_state.LcdEnabled) {
|
|
_lcdDisabled = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
template void GbPpu::ProcessOamCorruption<GbOamCorruptionType::Read>(uint16_t addr);
|
|
template void GbPpu::ProcessOamCorruption<GbOamCorruptionType::ReadIncDec>(uint16_t addr);
|
|
template void GbPpu::ProcessOamCorruption<GbOamCorruptionType::Write>(uint16_t addr);
|
|
|
|
template void GbPpu::Exec<true>();
|
|
template void GbPpu::Exec<false>();
|