1
0
Fork 0
2019-nes-emulator/Source/PPU/PPU.cpp

704 lines
14 KiB
C++

#include "PPU.hpp"
#include "NES.hpp"
const unsigned int BG_PALETTE_START_ADDRESS = 0x3F00u;
const unsigned int PATTERN_TABLE_0_START = 0x0000u;
const unsigned int PATTERN_TABLE_1_START = 0x1000u;
const unsigned int ATTRIBUTE_TABLE_START = 0x23C0u;
unsigned char reverse(uint8_t b)
{
b = (b & 0xF0u) >> 4u | (b & 0x0Fu) << 4u;
b = (b & 0xCCu) >> 2u | (b & 0x33u) << 2u;
b = (b & 0xAAu) >> 1u | (b & 0x55u) << 1u;
return b;
}
bool CheckBit(uint32_t value, uint32_t bit)
{
return (value & bit) == bit;
}
PPU::PPU()
{
// Visible Frame
{
for (int scanline = 0; scanline <= 239; ++scanline)
{
// Data fetching
for (int cycle = 1; cycle <= 256; cycle += 8)
{
events[scanline][cycle + 0] |= Event::FETCH_NT;
events[scanline][cycle + 2] |= Event::FETCH_AT;
events[scanline][cycle + 4] |= Event::FETCH_PT_LOW;
events[scanline][cycle + 6] |= Event::FETCH_PT_HIGH;
}
// Incrementing Hori(V)
for (int cycle = 8; cycle <= 256; cycle += 8)
{
events[scanline][cycle] |= Event::INCREMENT_TILE_X;
}
// Draw and shift
for (int cycle = 2; cycle <= 257; ++cycle)
{
events[scanline][cycle] |= Event::DRAW | Event::SHIFT_BACKGROUND;
}
// Reload shift registers
for (int cycle = 9; cycle <= 257; cycle += 8)
{
events[scanline][cycle] |= Event::LOAD_BACKGROUND;
}
// Inc Vert(v)
events[scanline][256] |= Event::INCREMENT_TILE_Y;
// Hori(v) = Hori(T)
events[scanline][257] |= Event::SET_TILE_X;
// Data fetching
for (int cycle = 321; cycle <= 336; cycle += 8)
{
events[scanline][cycle + 0] |= Event::FETCH_NT;
events[scanline][cycle + 2] |= Event::FETCH_AT;
events[scanline][cycle + 4] |= Event::FETCH_PT_LOW;
events[scanline][cycle + 6] |= Event::FETCH_PT_HIGH;
}
// Shift
for (int cycle = 322; cycle <= 337; ++cycle)
{
events[scanline][cycle] |= Event::SHIFT_BACKGROUND;
}
// Reload shift registers
for (int cycle = 329; cycle <= 337; cycle += 8)
{
events[scanline][cycle] |= Event::LOAD_BACKGROUND;
}
events[scanline][328] |= Event::INCREMENT_TILE_X;
events[scanline][336] |= Event::INCREMENT_TILE_X;
// Unused Fetches
events[scanline][337] |= Event::FETCH_NT;
events[scanline][339] |= Event::FETCH_NT;
}
}
// VBlank
{
events[241][1] |= Event::SET_VBLANK;
}
// Pre-render
{
// Clear VBlank, Sprite0 Hit, and Sprite Overflow
events[261][1] |= Event::CLEAR_FLAGS;
// Data fetching
for (int cycle = 1; cycle <= 256; cycle += 8)
{
events[261][cycle + 0] |= Event::FETCH_NT;
events[261][cycle + 2] |= Event::FETCH_AT;
events[261][cycle + 4] |= Event::FETCH_PT_LOW;
events[261][cycle + 6] |= Event::FETCH_PT_HIGH;
}
// Incrementing Hori(V)
for (int cycle = 8; cycle <= 256; cycle += 8)
{
events[261][cycle] |= Event::INCREMENT_TILE_X;
}
// Shift
for (int cycle = 2; cycle <= 257; ++cycle)
{
events[261][cycle] |= Event::SHIFT_BACKGROUND;
}
// Reload shift registers
for (int cycle = 9; cycle <= 257; cycle += 8)
{
events[261][cycle] |= Event::LOAD_BACKGROUND;
}
// Inc Vert(v)
events[261][256] |= Event::INCREMENT_TILE_Y;
// Hori(v) = Hori(t)
events[261][257] |= Event::SET_TILE_X;
// Vert(v) = Vert(t)
for (int cycle = 280; cycle <= 304; ++cycle)
{
events[261][cycle] |= Event::SET_TILE_Y;
}
// Data fetching
for (int cycle = 321; cycle <= 336; cycle += 8)
{
events[261][cycle + 0] |= Event::FETCH_NT;
events[261][cycle + 2] |= Event::FETCH_AT;
events[261][cycle + 4] |= Event::FETCH_PT_LOW;
events[261][cycle + 6] |= Event::FETCH_PT_HIGH;
}
// Shift
for (int cycle = 322; cycle <= 337; ++cycle)
{
events[261][cycle] |= Event::SHIFT_BACKGROUND;
}
// Reload shift registers
for (int cycle = 329; cycle <= 337; cycle += 8)
{
events[261][cycle] |= Event::LOAD_BACKGROUND;
}
events[261][328] |= Event::INCREMENT_TILE_X;
events[261][336] |= Event::INCREMENT_TILE_X;
// Unused Fetches
events[261][337] |= Event::FETCH_NT;
events[261][339] |= Event::FETCH_NT;
}
}
void PPU::Tick()
{
int event = events[currentScanline][currentCycle];
if (CheckBit(event, Event::DRAW))
{
Draw();
}
if (CheckBit(event, Event::SHIFT_BACKGROUND))
{
ShiftBG();
}
if (CheckBit(event, Event::FETCH_NT))
{
FetchNT();
}
else if (CheckBit(event, Event::FETCH_AT))
{
FetchAT();
}
else if (CheckBit(event, Event::FETCH_PT_LOW))
{
FetchPTLow();
}
else if (CheckBit(event, Event::FETCH_PT_HIGH))
{
FetchPTHigh();
}
if (CheckBit(event, Event::LOAD_BACKGROUND))
{
LoadBackground();
}
if (CheckBit(event, Event::SET_TILE_X))
{
SetTileX();
}
else if (CheckBit(event, Event::SET_TILE_Y))
{
SetTileY();
}
if (CheckBit(event, Event::INCREMENT_TILE_Y))
{
IncrementTileY();
}
else if (CheckBit(event, Event::INCREMENT_TILE_X))
{
IncrementTileX();
}
if (CheckBit(event, Event::SET_VBLANK))
{
SetVBlank();
}
else if (CheckBit(event, Event::CLEAR_FLAGS))
{
ClearFlags();
}
++currentCycle;
// Odd frame, skip
if (currentScanline == 261 && currentCycle == 340 && (frameNumber % 2))
{
currentScanline = 0;
currentCycle = 0;
++frameNumber;
}
else if (currentCycle == 341)
{
currentCycle = 0;
++currentScanline;
if (currentScanline == 262)
{
currentScanline = 0;
++frameNumber;
}
}
}
void PPU::WriteRegister(Register reg, uint8_t value)
{
switch (reg)
{
case Register::PPUCTRL:
{
regPPUCTRL.SetByte(value);
// yyy NNYY YYYX XXXX
// t: --- BA-- ---- ---- = d: ---- --BA
tempAddress.nametableY = (value & 0x2u) >> 1u;
tempAddress.nametableX = (value & 0x1u) >> 1u;
break;
}
case Register::PPUMASK:
{
regPPUMASK.SetByte(value);
break;
}
case Register::OAMADDR:
{
regOAMADDR = value;
break;
}
case Register::OAMDATA:
{
oam[regOAMADDR] = value;
++regOAMADDR;
break;
}
case Register::PPUSCROLL:
{
if (firstWrite)
{
// yyy NNYY YYYX XXXX
// t: --- ---- ---H GFED = d: HGFE D---
// x: CBA = d: ---- -CBA
// w: = 1
tempAddress.scrollCoarseX = (value & 0xF8u) >> 3u;
scrollFineX = value & 0x7u;
firstWrite = false;
}
else
{
// yyy NNYY YYYX XXXX
// t: CBA --HG FED- ---- = d: HGFE DCBA
// w: = 0
tempAddress.scrollCoarseY = (value & 0xF8u) >> 3u;
tempAddress.scrollFineY = value & 0x7u;
firstWrite = true;
}
break;
}
case Register::PPUADDR:
{
// MSB
if (firstWrite)
{
// yyy NNYY YYYX XXXX
// t: -FE DCBA ---- ---- = d: --FE DCBA
// t: X-- ---- ---- ---- = 0
// w: = 1
tempAddress.scrollCoarseY &= ~(0x18u);
tempAddress.scrollCoarseY |= (value & 0x3u) << 3u;
tempAddress.nametableY = (value & 0x8u) >> 3u;
tempAddress.nametableX = (value & 0x4u) >> 2u;
tempAddress.scrollFineY = (value & 0x30u) >> 4u;
tempAddress.scrollFineY &= ~(0x4u);
firstWrite = false;
}
// LSB
else
{
// yyy NNYY YYYX XXXX
// t: ... .... HGFE DCBA = d: HGFE DCBA
// v = t
// w: = 0
tempAddress.scrollCoarseY &= ~(0x7u);
tempAddress.scrollCoarseY |= (value & 0xE0u) >> 5u;
tempAddress.scrollCoarseX = value & 0x1Fu;
currentAddress = tempAddress;
firstWrite = true;
}
break;
}
case Register::PPUDATA:
{
WriteMemory(currentAddress.GetValue(), value);
uint16_t temp = currentAddress.GetValue();
temp += (regPPUCTRL.vramAddrIncrement) ? 32u : 1u;
currentAddress.SetValue(temp);
break;
}
default: break;
}
}
uint8_t PPU::ReadRegister(Register reg)
{
uint8_t byte{};
switch (reg)
{
case Register::PPUSTATUS:
{
byte = regPPUSTATUS.GetByte();
regPPUSTATUS.vblankStarted = 0u;
firstWrite = true;
break;
}
case Register::OAMDATA:
{
byte = oam[regOAMADDR];
break;
}
case Register::PPUDATA:
{
// Returned byte is buffered from last read of PPUDATA
byte = oldByte;
oldByte = ReadMemory(currentAddress.GetValue());
// If reading palette data, return actual byte instead of buffered byte
if (currentAddress.GetValue() >= BG_PALETTE_START_ADDRESS)
{
byte = oldByte;
}
uint16_t temp = currentAddress.GetValue();
temp += (regPPUCTRL.vramAddrIncrement) ? 32u : 1u;
currentAddress.SetValue(temp);
break;
}
default: break;
}
return byte;
}
void PPU::WriteMemory(uint16_t address, uint8_t value)
{
nes->Write(BusSource::PPU, address, value);
}
uint8_t PPU::ReadMemory(uint16_t address)
{
return nes->Read(BusSource::PPU, address);
}
void PPU::FetchNT()
{
if (!regPPUMASK.showBackground)
{return;}
uint16_t address = 0x2000u | (currentAddress.scrollCoarseY << 5u) | currentAddress.scrollCoarseX;
nametableLatch = ReadMemory(address);
}
void PPU::FetchAT()
{
if (!regPPUMASK.showBackground)
{return;}
// Bits 4,3,2 of Coarse Y Scroll are Bits 5,4,3 of attribute table address
// Bits 4,3,2 of Coarse X Scroll are Bits 2,1,0 of attribute table address
uint16_t address = ATTRIBUTE_TABLE_START
| ((currentAddress.scrollCoarseY & 0x1Cu) << 1u)
| ((currentAddress.scrollCoarseX & 0x1Cu) >> 2u);
// Bit 1 of Coarse Y Scroll is Block Row (0 or 1)
// Bit 1 of Coarse X Scroll is Block Col (0 or 1)
// Y determines if top or bottom
// X determines if left or right
uint8_t at = ReadMemory(address);
uint8_t location = ((currentAddress.scrollCoarseY & 0x2u))
| ((currentAddress.scrollCoarseX & 0x2u) >> 1u);
switch (location)
{
// Top Left
case 0:
{
attributeTableLatch = at & 0x03u;
break;
}
// Top Right
case 1:
{
attributeTableLatch = (at & 0x0Cu) >> 2u;
break;
}
// Bottom Left
case 2:
{
attributeTableLatch = (at & 0x30u) >> 4u;
break;
}
// Bottom Right
case 3:
{
attributeTableLatch = (at & 0xC0u) >> 6u;
break;
}
default: break;
}
}
void PPU::FetchPTLow()
{
if (!regPPUMASK.showBackground)
{return;}
uint16_t address = regPPUCTRL.bgPatternTableAddr ? PATTERN_TABLE_1_START : PATTERN_TABLE_0_START;
address += (nametableLatch * 16u) + currentAddress.scrollFineY;
patternTableLowLatch = ReadMemory(address);
}
void PPU::FetchPTHigh()
{
if (!regPPUMASK.showBackground)
{return;}
uint16_t address = regPPUCTRL.bgPatternTableAddr ? PATTERN_TABLE_1_START : PATTERN_TABLE_0_START;
address += (nametableLatch * 16u) + 8u + currentAddress.scrollFineY;
patternTableHighLatch = ReadMemory(address);
}
void PPU::SetVBlank()
{
regPPUSTATUS.vblankStarted = 1u;
if (regPPUCTRL.nmiEnable)
{
nes->nmi = true;
}
}
void PPU::SetTileX()
{
if (!regPPUMASK.showBackground)
{return;}
// yyy NNYY YYYX XXXX
// v: --- -F-- ---E DCBA = t: --- -F-- ---E DCBA
currentAddress.nametableX = tempAddress.nametableX;
currentAddress.scrollCoarseX = tempAddress.scrollCoarseX;
}
void PPU::SetTileY()
{
if (!regPPUMASK.showBackground)
{return;}
// yyy NNYY YYYX XXXX
// v: IHG F-ED CBA- ---- = t: IHG F-ED CBA- ----
currentAddress.scrollCoarseY = tempAddress.scrollCoarseY;
currentAddress.nametableY = tempAddress.nametableY;
currentAddress.scrollFineY = tempAddress.scrollFineY;
}
void PPU::Draw()
{
if (!regPPUMASK.showBackground)
{return;}
// Draw occurs on cycle 2
uint8_t x = currentCycle - 2;
uint8_t h = (patternTableHighShift & 0x8000u) >> (15u - scrollFineX);
uint8_t l = (patternTableLowShift & 0x8000u) >> (15u - scrollFineX);
uint8_t bgColor = (h << 1u) | l;
uint8_t paletteIndexHigh = (atShiftHigh & 0x80u) >> (7u - scrollFineX);
uint8_t paletteIndexLow = (atShiftLow & 0x80u) >> (7u - scrollFineX);
uint8_t pIndex = (paletteIndexHigh << 1u) | paletteIndexLow;
uint8_t paletteIndex = palette[pIndex][bgColor];
uint32_t color = nes->palette[paletteIndex].GetValue();
video[currentScanline * VIDEO_WIDTH + x] = color;
}
void PPU::LoadBackground()
{
if (!regPPUMASK.showBackground)
{return;}
patternTableHighShift &= ~(0xFFu);
patternTableHighShift |= (patternTableHighLatch);
patternTableLowShift &= ~(0xFFu);
patternTableLowShift |= (patternTableLowLatch);
attribute = attributeTableLatch;
}
void PPU::IncrementTileX()
{
if (!regPPUMASK.showBackground)
{return;}
// Increment coarse X scroll and wrap if needed
if (currentAddress.scrollCoarseX == 31)
{
currentAddress.scrollCoarseX = 0;
currentAddress.nametableX = currentAddress.nametableX ? 0u : 1u;
}
else
{
++currentAddress.scrollCoarseX;
}
}
void PPU::IncrementTileY()
{
if (!regPPUMASK.showBackground)
{return;}
// Increment Y scroll and wrap if needed
if (currentAddress.scrollFineY < 7)
{
++currentAddress.scrollFineY;
}
else
{
currentAddress.scrollFineY = 0;
if (currentAddress.scrollCoarseY == 29)
{
currentAddress.scrollCoarseY = 0;
currentAddress.nametableY = currentAddress.nametableY ? 0u : 1u;
}
else if (currentAddress.scrollCoarseY == 31)
{
currentAddress.scrollCoarseY = 0;
}
else
{
++currentAddress.scrollCoarseY;
}
}
}
void PPU::ClearFlags()
{
regPPUSTATUS.vblankStarted = 0u;
regPPUSTATUS.spriteOverflow = 0u;
regPPUSTATUS.sprite0Hit = 0u;
}
void PPU::ShiftBG()
{
if (!regPPUMASK.showBackground)
{return;}
patternTableHighShift <<= 1u;
patternTableLowShift <<= 1u;
atShiftLow <<= 1u;
atShiftHigh <<= 1u;
atShiftLow |= attribute & 0x1u;
atShiftHigh |= (attribute & 0x2u) >> 1u;
}
uint8_t PPU::PpuStatus::GetByte()
{
uint8_t byte = vblankStarted << 7u | sprite0Hit << 6u | spriteOverflow << 5u | previousLsb;
return byte;
}
void PPU::PpuMask::SetByte(uint8_t byte)
{
blueEmphasis = (byte & 0x80u) >> 7u;
greenEmphasis = (byte & 0x40u) >> 6u;
redEmphasis = (byte & 0x20u) >> 5u;
showSprites = (byte & 0x10u) >> 4u;
showBackground = (byte & 0x08u) >> 3u;
showSpritesLeft = (byte & 0x04u) >> 2u;
showBackgroundLeft = (byte & 0x02u) >> 1u;
grayscale = byte & 0x01u;
}
void PPU::PpuCtrl::SetByte(uint8_t byte)
{
nmiEnable = (byte & 0x80u) >> 7u;
masterSlaveSelect = (byte & 0x40u) >> 6u;
spriteSize = (byte & 0x20u) >> 5u;
bgPatternTableAddr = (byte & 0x10u) >> 4u;
spritePatternTableAddr = (byte & 0x08u) >> 3u;
vramAddrIncrement = (byte & 0x04u) >> 2u;
nametableBaseAddr = ((byte & 0x02u) >> 1u | (byte & 0x01u));
}
void PPU::Address::SetValue(uint16_t value)
{
// yyy NNYY YYYX XXXX
scrollFineY = (value & 0x7000u) >> 12u;
nametableX = (value & 0x0800u) >> 11u;
nametableY = (value & 0x0400u) >> 10u;
scrollCoarseY = (value & 0x03E0u) >> 5u;
scrollCoarseX = value & 0x0001Fu;
}
uint16_t PPU::Address::GetValue()
{
// yyy NNYY YYYX XXXX
return (scrollFineY << 12u) | (nametableY << 11u) | (nametableX << 10u) | (scrollCoarseY << 5u) | scrollCoarseX;
}