704 lines
14 KiB
C++
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;
|
|
}
|
|
|