You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
439 lines
16 KiB
439 lines
16 KiB
/* GameBoyInstance.Video.cpp |
|
* |
|
* Simulated graphics hardware for the GameBoy core. |
|
*/ |
|
|
|
#include <cstring> |
|
#include <sstream> |
|
|
|
#include "../../PlipEmulationException.h" |
|
#include "../../PlipUtility.h" |
|
|
|
#include "GameBoyInstance.h" |
|
|
|
namespace Plip::Core::GameBoy { |
|
void GameBoyInstance::VideoCycle() { |
|
bool result; |
|
|
|
switch(m_videoMode) { |
|
case m_videoModeHBlank: |
|
result = VideoHBlank(); |
|
break; |
|
case m_videoModeVBlank: |
|
result = VideoVBlank(); |
|
break; |
|
case m_videoModeOamSearch: |
|
result = VideoOamSearch(); |
|
break; |
|
case m_videoModePicGen: |
|
result = VideoVidGen(); |
|
break; |
|
default: |
|
std::stringstream ex; |
|
ex << "Invalid video mode: " |
|
<< PlipUtility::FormatHex(m_videoMode, 1) << "\n\n" |
|
<< m_cpu->DumpRegisters(); |
|
throw Plip::PlipEmulationException(ex.str().c_str()); |
|
} |
|
|
|
if(result) { |
|
m_dotCount += 1; |
|
} else { |
|
// Mode transition. |
|
VideoModePreTransition(); |
|
m_videoMode = ++m_videoMode > 3 ? 0 : m_videoMode; |
|
if(m_videoMode == 1 && m_videoLy < m_screenHeight) |
|
m_videoMode++; // only transition to 1 after the last HBlank |
|
VideoModePostTransition(); |
|
} |
|
|
|
auto stat = m_ioRegisters->GetByte(m_regLcdcStatus) & 0b11111000; |
|
stat |= (m_videoCoincidence << 2) | m_videoMode; |
|
m_ioRegisters->SetByte(m_regLcdcStatus, stat); |
|
} |
|
|
|
void GameBoyInstance::VideoExecute() { |
|
auto lcdc = m_ioRegisters->GetByte(m_regLcdControl); |
|
|
|
// Run 4 dot clock cycles (4.19MHz) if the display is enabled. |
|
if(BIT_TEST(m_videoLastLcdc, 7) && !BIT_TEST(lcdc, 7)) { |
|
// The LCD display was disabled during this CPU cycle. Flag all |
|
// video memory as being writable. |
|
m_oam->SetWritable(true); |
|
m_videoRam->SetWritable(true); |
|
|
|
// When the LCD is disabled, the screen should go blank. |
|
memset(m_videoBuffer, 0xFF, m_videoBufferSize); |
|
m_video->BeginDraw(); |
|
m_video->Draw(m_videoBuffer); |
|
m_video->EndDraw(); |
|
m_video->Render(); |
|
} else if(!BIT_TEST(m_videoLastLcdc, 7) && BIT_TEST(lcdc, 7)) { |
|
// The LCD display was enabled during this CPU cycle. Set the |
|
// video memory accessibility appropriately. |
|
VideoSetMemoryPermissions(); |
|
m_lcdBlankFrame = true; |
|
} |
|
|
|
if(BIT_TEST(lcdc, 7)) { |
|
for(auto dotCycle = 0; dotCycle < m_dotsPerCycle; dotCycle++) { |
|
VideoCycle(); |
|
} |
|
} |
|
|
|
m_ioRegisters->SetByte(m_regLy, m_videoLy); |
|
m_videoLastLcdc = lcdc; |
|
} |
|
|
|
bool GameBoyInstance::VideoHBlank() const { |
|
return m_dotCount < m_videoScanlineTime; |
|
} |
|
|
|
bool GameBoyInstance::VideoOamSearch() { |
|
if(m_dotCount != 0) return m_dotCount < m_videoOamScanTime; |
|
|
|
auto spriteHeight = ((m_ioRegisters->GetByte(m_regLcdControl) >> 2) & 1) ? 16 : 8; |
|
|
|
// Just do everything at once since the OAM will be locked at this point. |
|
m_spriteListIdx = 0; |
|
memset(m_spriteList, 0xFF, m_maxSpritesPerScanline); |
|
for(auto sprIdx = 0; sprIdx < m_maxSpritesInOam; sprIdx++) { |
|
auto sprAddr = 4 * sprIdx; |
|
auto sprY = m_oam->GetByte(sprAddr); |
|
auto sprX = m_oam->GetByte(sprAddr + 1); |
|
|
|
if(sprX == 0 || sprX >= m_screenWidth + 8) continue; |
|
if(m_videoLy < sprY - 16 || m_videoLy >= sprY - (16 - spriteHeight)) |
|
continue; |
|
|
|
m_spriteList[m_spriteListIdx++] = sprIdx; |
|
if(m_spriteListIdx == m_maxSpritesPerScanline) break; |
|
} |
|
|
|
return m_dotCount < m_videoOamScanTime; |
|
} |
|
|
|
void GameBoyInstance::VideoModePostTransition() { |
|
uint8_t stat = m_ioRegisters->GetByte(m_regLcdcStatus); |
|
VideoSetMemoryPermissions(); |
|
|
|
// New mode. |
|
switch(m_videoMode) { |
|
case m_videoModeHBlank: |
|
if(BIT_TEST(stat, 3)) m_cpu->Interrupt(INTERRUPT_LCDSTAT); |
|
break; |
|
|
|
case m_videoModeVBlank: |
|
m_dotCount = 0; |
|
|
|
// Plot the current buffer to the screen and flip buffers. |
|
m_video->BeginDraw(); |
|
m_video->Draw(m_videoBuffer); |
|
m_video->EndDraw(); |
|
m_video->Render(); |
|
|
|
m_lcdBlankFrame = false; |
|
|
|
m_cpu->Interrupt(INTERRUPT_VBLANK); |
|
if(BIT_TEST(stat, 4)) m_cpu->Interrupt(INTERRUPT_LCDSTAT); |
|
break; |
|
|
|
case m_videoModeOamSearch: |
|
memset(m_spriteList, 0x00, m_maxSpritesPerScanline); |
|
m_dotCount = 0; |
|
m_videoCoincidence = m_videoLy == m_ioRegisters->GetByte(m_regLyCompare) ? 1 : 0; |
|
|
|
if(BIT_TEST(stat, 5)) // OAM interrupt |
|
m_cpu->Interrupt(INTERRUPT_LCDSTAT); |
|
|
|
if(m_videoCoincidence && BIT_TEST(stat, 6)) // LYC=LY interrupt. |
|
m_cpu->Interrupt(INTERRUPT_LCDSTAT); |
|
|
|
break; |
|
|
|
case m_videoModePicGen: |
|
m_dotCount = 0; |
|
m_vidGenStage = BackgroundScrolling; |
|
break; |
|
|
|
default: |
|
std::stringstream ex; |
|
ex << "Invalid video mode: " |
|
<< PlipUtility::FormatHex(m_videoMode, 1) << "\n\n" |
|
<< m_cpu->DumpRegisters(); |
|
throw Plip::PlipEmulationException(ex.str().c_str()); |
|
} |
|
} |
|
|
|
void GameBoyInstance::VideoModePreTransition() { |
|
// Previous mode. |
|
switch(m_videoMode) { |
|
case m_videoModeHBlank: |
|
m_videoLx = 0; |
|
m_videoLy++; |
|
break; |
|
case m_videoModeVBlank: |
|
m_videoLy = 0; |
|
break; |
|
case m_videoModeOamSearch: |
|
case m_videoModePicGen: |
|
break; |
|
default: |
|
std::stringstream ex; |
|
ex << "Invalid video mode: " |
|
<< PlipUtility::FormatHex(m_videoMode, 1) << "\n\n" |
|
<< m_cpu->DumpRegisters(); |
|
throw Plip::PlipEmulationException(ex.str().c_str()); |
|
} |
|
} |
|
|
|
void GameBoyInstance::VideoOamDmaTransfer() { |
|
auto srcAddr = (m_ioRegisters->GetByte(m_regOamDmaTransfer, true) << 8) | m_videoOamDmaTransferIdx; |
|
m_oam->SetByte(m_videoOamDmaTransferIdx++, m_memory->GetByte(srcAddr, true), true); |
|
} |
|
|
|
void GameBoyInstance::VideoOamDmaTransferEnd() { |
|
// The DMA transfer is finished. Set the RAM accessibility back |
|
// to a normal state. |
|
m_videoOamDmaTransferIdx = -1; |
|
|
|
if(m_cartRam != nullptr) { |
|
m_cartRam->SetReadable(true); |
|
m_cartRam->SetWritable(true); |
|
} |
|
|
|
m_rom->SetReadable(true); |
|
|
|
m_workRam->SetReadable(true); |
|
m_workRam->SetWritable(true); |
|
|
|
m_videoRam->SetReadable(true); |
|
m_videoRam->SetWritable(true); |
|
|
|
m_oam->SetReadable(true); |
|
m_oam->SetWritable(true); |
|
|
|
m_ioRegisters->SetReadable(true); |
|
m_ioRegisters->SetWritable(true); |
|
} |
|
|
|
void GameBoyInstance::VideoOamDmaTransferStart() { |
|
// Set up the system for the DMA transfer. |
|
m_videoOamDmaTransferIdx = 0; |
|
|
|
if(m_cartRam != nullptr) { |
|
m_cartRam->SetReadable(false); |
|
m_cartRam->SetWritable(false); |
|
} |
|
|
|
m_rom->SetReadable(false); |
|
|
|
m_workRam->SetReadable(false); |
|
m_workRam->SetWritable(false); |
|
|
|
m_videoRam->SetReadable(false); |
|
m_videoRam->SetWritable(false); |
|
|
|
m_oam->SetReadable(false); |
|
m_oam->SetWritable(false); |
|
|
|
m_ioRegisters->SetReadable(false); |
|
m_ioRegisters->SetWritable(false); |
|
} |
|
|
|
void GameBoyInstance::VideoSetMemoryPermissions() { |
|
switch(m_videoMode) { |
|
case m_videoModeHBlank: |
|
case m_videoModeVBlank: |
|
m_oam->SetWritable(true); |
|
m_videoRam->SetWritable(true); |
|
break; |
|
case m_videoModeOamSearch: |
|
m_oam->SetWritable(false); |
|
m_videoRam->SetWritable(true); |
|
break; |
|
case m_videoModePicGen: |
|
m_oam->SetWritable(false); |
|
m_videoRam->SetWritable(false); |
|
break; |
|
default: |
|
std::stringstream ex; |
|
ex << "Invalid video mode: " |
|
<< PlipUtility::FormatHex(m_videoMode, 1) << "\n\n" |
|
<< m_cpu->DumpRegisters(); |
|
throw Plip::PlipEmulationException(ex.str().c_str()); |
|
} |
|
} |
|
|
|
bool GameBoyInstance::VideoVBlank() { |
|
if(m_dotCount > 0) |
|
if(m_dotCount % (m_videoOamScanTime + m_videoScanlineTime) == 0) |
|
m_videoLy++; |
|
|
|
return m_dotCount < m_videoVBlankTime; |
|
} |
|
|
|
bool GameBoyInstance::VideoVidGen() { |
|
uint8_t scx; |
|
|
|
switch(m_vidGenStage) { |
|
case BackgroundScrolling: |
|
// Pause the dot clock to simulate the background shifter. |
|
scx = m_ioRegisters->GetByte(m_regScx); |
|
if(++m_vidGenTick > scx % 8) { |
|
m_vidGenTick = 0; |
|
m_vidGenStage = Drawing; |
|
} |
|
break; |
|
|
|
case Drawing: |
|
VideoVidGenDraw(); |
|
m_videoLx++; |
|
|
|
break; |
|
|
|
default: |
|
std::stringstream ex; |
|
ex << "Invalid video generation stage: " |
|
<< PlipUtility::FormatHex((int)m_vidGenStage, 1) << "\n\n" |
|
<< m_cpu->DumpRegisters(); |
|
throw Plip::PlipEmulationException(ex.str().c_str()); |
|
} |
|
|
|
return m_videoLx < m_screenWidth; |
|
} |
|
|
|
#pragma clang diagnostic push |
|
#pragma ide diagnostic ignored "readability-function-cognitive-complexity" |
|
inline void GameBoyInstance::VideoVidGenDraw() { |
|
// TODO: Add timing routine. |
|
uint8_t pixelColor, pixelDataLow, pixelDataHigh, pixelDataCombined; |
|
uint8_t tileIdx, tilePX, tilePY, tileShift; |
|
uint8_t lineOffset; |
|
|
|
auto pos = (m_videoLy * m_screenWidth) + m_videoLx; |
|
|
|
auto lcdc = m_ioRegisters->GetByte(m_regLcdControl); |
|
bool lcdEnabled = BIT_TEST(lcdc, 7); |
|
if(!lcdEnabled) { |
|
Plot(255, pos); |
|
return; |
|
} |
|
|
|
auto scx = m_ioRegisters->GetByte(m_regScx); |
|
auto scy = m_ioRegisters->GetByte(m_regScy); |
|
auto bgp = m_ioRegisters->GetByte(m_regBgp); |
|
|
|
if(BIT_TEST(lcdc, 0)) { // BG/Window Display Enabled |
|
auto tileDataAddr = m_vramTileBase + (BIT_TEST(lcdc, 4) ? 0 : m_vramTileBlockOffset); |
|
auto tileMapAddr = m_vramBgBase + (BIT_TEST(lcdc, 3) ? m_vramBgBlockOffset : 0); |
|
auto tileX = ((scx + m_videoLx) / 8) % 32; |
|
auto tileY = ((scy + m_videoLy) / 8) % 32; |
|
tilePX = (scx + m_videoLx) % 8; |
|
tilePY = (scy + m_videoLy) % 8; |
|
|
|
auto mapIdx = (tileY * 32) + tileX; |
|
tileIdx = m_videoRam->GetByte(tileMapAddr + mapIdx); |
|
|
|
lineOffset = tilePY * 2; |
|
|
|
pixelDataLow = m_videoRam->GetByte(tileDataAddr + (tileIdx * 16) + lineOffset, true); |
|
pixelDataHigh = m_videoRam->GetByte(tileDataAddr + (tileIdx * 16) + lineOffset + 1, true); |
|
tileShift = 7 - tilePX; |
|
pixelDataCombined = (((pixelDataHigh >> tileShift) & 0b1) << 1) |
|
| ((pixelDataLow >> tileShift) & 0b1); |
|
|
|
pixelColor = (bgp >> (pixelDataCombined * 2)) & 0b11; |
|
Plot(pixelColor, pos); |
|
|
|
if(BIT_TEST(lcdc, 5)) { // Window Display Enabled |
|
auto wx = m_ioRegisters->GetByte(m_regWx) - 7; |
|
auto wy = m_ioRegisters->GetByte(m_regWy); |
|
|
|
if(!(wx > 166 || wy > 143) || m_videoLx >= wx || m_videoLy >= wy) { |
|
tileMapAddr = m_vramBgBase + (BIT_TEST(lcdc, 6) ? m_vramBgBlockOffset : 0); |
|
tileX = ((m_videoLx + wx) / 8) % 32; |
|
tileY = ((m_videoLy + wy) / 8) % 32; |
|
tilePX = (m_videoLx + wx) % 8; |
|
tilePY = (m_videoLy + wy) % 8; |
|
|
|
mapIdx = (tileY * 32) + tileX; |
|
tileIdx = m_videoRam->GetByte(tileMapAddr + mapIdx, true); |
|
|
|
lineOffset = tilePY * 2; |
|
|
|
pixelDataLow = m_videoRam->GetByte(tileDataAddr + (tileIdx * 16) + lineOffset, true); |
|
pixelDataHigh = m_videoRam->GetByte(tileDataAddr + (tileIdx * 16) + lineOffset + 1, true); |
|
tileShift = 7 - tilePX; |
|
pixelDataCombined = (((pixelDataHigh >> tileShift) & 0b1) << 1) |
|
| ((pixelDataLow >> tileShift) & 0b1); |
|
|
|
pixelColor = (bgp >> (pixelDataCombined * 2)) & 0b11; |
|
Plot(pixelColor, pos); |
|
} |
|
} |
|
} else { |
|
// BG/Window display disabled. Draw a white pixel. |
|
Plot(0b00, pos); |
|
} |
|
|
|
if(!BIT_TEST(lcdc, 1)) return; |
|
|
|
// OBJ Display Enabled |
|
auto obp0 = m_ioRegisters->GetByte(m_regObp0); |
|
auto obp1 = m_ioRegisters->GetByte(m_regObp1); |
|
bool doubleHeight = BIT_TEST(lcdc, 2); |
|
|
|
for(auto i = 0; i < m_maxSpritesPerScanline; i++) { |
|
if(m_spriteList[i] == 0xFF) break; |
|
|
|
auto base = 4 * m_spriteList[i]; |
|
|
|
// Fetch the sprite attributes from the OAM. |
|
auto sprAttr = m_oam->GetByte(base + 3, true); |
|
bool sprFlipX = BIT_TEST(sprAttr, 5); |
|
bool sprFlipY = BIT_TEST(sprAttr, 6); |
|
bool sprPriority = !BIT_TEST(sprAttr, 7); |
|
auto sprPalette = BIT_TEST(sprAttr, 4) ? obp1 : obp0; |
|
|
|
// Position and tile number. |
|
auto sprY = m_oam->GetByte(base, true); |
|
auto sprX = m_oam->GetByte(base + 1, true); |
|
tileIdx = m_oam->GetByte(base + 2, true); |
|
if(doubleHeight) tileIdx &= 0b11111110; // LSB is ignored for 8x16 sprites. |
|
|
|
// Check to see if the sprite should even be drawn. |
|
if(m_videoLx < sprX - 8 || m_videoLx >= sprX) continue; // X value out of range. |
|
if(!sprPriority && pixelColor > 0) continue; // Background/window overlaps sprite. |
|
|
|
// Figure out which pixel should be considered. |
|
auto sprHeight = doubleHeight ? 16 : 8; |
|
tilePX = m_videoLx - (sprX - 8); |
|
if(sprFlipX) tilePX += -7; |
|
if(sprFlipY) tilePY += -(doubleHeight ? 15 : 7); |
|
tilePY = m_videoLy - (sprY - 16); |
|
if(doubleHeight && tilePY >= 8) { |
|
// Move to the second tile of the 8x16 sprite. |
|
tilePY %= 8; |
|
tileIdx |= 0b1; |
|
} else if(doubleHeight && tilePY < 8) { |
|
// Move to the first tile (this will likely only be used if the sprite |
|
// is flipped). |
|
tileIdx &= 0b11111110; |
|
} |
|
lineOffset = tilePY * 2; |
|
|
|
pixelDataLow = m_videoRam->GetByte(m_vramTileBase + (tileIdx * 16) + lineOffset, true); |
|
pixelDataHigh = m_videoRam->GetByte(m_vramTileBase + (tileIdx * 16) + lineOffset + 1, true); |
|
tileShift = 7 - tilePX; |
|
pixelDataCombined = (((pixelDataHigh >> tileShift) & 0b1) << 1) |
|
| ((pixelDataLow >> tileShift) & 0b1); |
|
|
|
if(pixelDataCombined == 0) continue; // Transparent pixel. |
|
|
|
pixelColor = (sprPalette >> (pixelDataCombined * 2)) & 0b11; |
|
Plot(pixelColor, pos); |
|
} |
|
} |
|
#pragma clang diagnostic pop |
|
}
|
|
|