#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; }