From 352c1588ae2499f7e11acc0353fab96e72ce4d98 Mon Sep 17 00:00:00 2001 From: Selim Mustafaev Date: Sun, 24 Sep 2023 23:14:34 +0300 Subject: [PATCH] Finised sprite rendering --- CMakeLists.txt | 4 +- main.cpp | 26 ++++++--- src/Cartridge.cpp | 4 ++ src/Cartridge.h | 27 ++++++--- src/Cpu.cpp | 4 +- src/Logger.cpp | 4 +- src/Oam.cpp | 144 ++++++++++++++++++++++++++++++++++++++++++++++ src/Oam.h | 61 ++++++++++++++++++++ src/Ppu.cpp | 118 ++++++++++++++++++++++++++++++------- src/Ppu.h | 21 ++----- src/Shifter.cpp | 12 +++- src/Shifter.h | 4 +- 12 files changed, 369 insertions(+), 60 deletions(-) create mode 100644 src/Oam.cpp create mode 100644 src/Oam.h diff --git a/CMakeLists.txt b/CMakeLists.txt index f5c1d56..670dcc2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,7 +26,9 @@ add_executable(nes src/Controller.cpp src/Controller.h src/SdlKeyboardController.cpp - src/SdlKeyboardController.h src/Dma.cpp src/Dma.h) + src/SdlKeyboardController.h src/Dma.cpp src/Dma.h + src/Oam.cpp + src/Oam.h) find_package(SDL2 CONFIG REQUIRED) find_package(fmt REQUIRED) diff --git a/main.cpp b/main.cpp index 9189efe..d9b5d25 100644 --- a/main.cpp +++ b/main.cpp @@ -3,7 +3,6 @@ #include "src/SdlKeyboardController.h" #include -#include #include int main() { @@ -15,22 +14,31 @@ int main() { window.setSize(nes::Ppu::SCREEN_WIDTH * 4, nes::Ppu::SCREEN_HEIGHT * 4); SDL_Event e; - device.setNewFrameCallback([&window, &e](auto buffer){ + bool frameRendered = false; + device.setNewFrameCallback([&window, &e, &frameRendered](auto buffer){ window.drawFrame(buffer); while(SDL_PollEvent(&e)); + frameRendered = true; }); device.connect(std::make_shared()); - device.insertCartridge("/home/selim/Downloads/dk.nes"); - //device.insertCartridge("/Users/selim/Documents/nestest.nes"); + //device.insertCartridge("/home/selim/Downloads/dk.nes"); + device.insertCartridge("/Users/selim/Documents/smb.nes"); //device.insertCartridge("C:\\Users\\selim\\Documents\\nestest.nes"); - uint64_t cycles = 0; - while (cycles < 1000000000) { + auto frameStart = std::chrono::steady_clock::now(); + while (true) { device.tick(); - cycles++; - //int64_t us = static_cast(1000000000.0/(60*nes::Ppu::SCREEN_WIDTH*nes::Ppu::SCREEN_HEIGHT)); - //std::this_thread::sleep_for(std::chrono::nanoseconds(1)); + if(frameRendered) { + auto renderingTime = std::chrono::steady_clock::now(); + auto elapsedTime = std::chrono::duration_cast(renderingTime - frameStart).count(); + int64_t waitTime = 1000000000/60 - elapsedTime; + if(waitTime > 0) { + std::this_thread::sleep_for(std::chrono::nanoseconds(waitTime)); + } + frameStart = std::chrono::steady_clock::now(); + frameRendered = false; + } } return 0; diff --git a/src/Cartridge.cpp b/src/Cartridge.cpp index aa94dc3..241adfe 100644 --- a/src/Cartridge.cpp +++ b/src/Cartridge.cpp @@ -40,4 +40,8 @@ namespace nes { return _chrRom[mappedAddress]; } + Cartridge::Mirroring Cartridge::mirroring() const { + return _header->flags.mirroring == 0 ? Mirroring::Horizontal : Mirroring::Vertical; + } + } diff --git a/src/Cartridge.h b/src/Cartridge.h index 1d3f47e..7ef7e95 100644 --- a/src/Cartridge.h +++ b/src/Cartridge.h @@ -19,20 +19,33 @@ namespace nes { constexpr size_t PRG_CHUNK_SIZE = 16*1024; constexpr size_t CHR_CHUNK_SIZE = 8*1024; - struct RomHeader { - uint32_t magic; - uint8_t prgChunks; - uint8_t chrChunks; - uint8_t reserved[10]; - }; - class Cartridge { + public: + enum Mirroring { + Horizontal, + Vertical + }; + + struct Flags { + uint8_t mirroring: 1; + uint8_t reserved: 7; + }; + + struct RomHeader { + uint32_t magic; + uint8_t prgChunks; + uint8_t chrChunks; + Flags flags; + uint8_t reserved[9]; + }; + public: explicit Cartridge(const fs::path& path); public: uint8_t readPrg(uint16_t address); uint8_t readChr(uint16_t address); + Mirroring mirroring() const; private: std::unique_ptr _romData; diff --git a/src/Cpu.cpp b/src/Cpu.cpp index 01a6aef..d9d21b2 100644 --- a/src/Cpu.cpp +++ b/src/Cpu.cpp @@ -5,7 +5,7 @@ #include "Cpu.h" #include "Nes.h" #include -#include +#include namespace nes { @@ -224,7 +224,7 @@ namespace nes { } std::string Cpu::state() const { - return std::format("{} ({:02X}), PC: {:X}, SP: {:X}, A: {:02X}, X: {:02X}, Y: {:02X}, [N:{}, V:{}, B{}, D{}, I{}, Z:{}, C:{}], H: {:08X}", + return fmt::format("{} ({:02X}), PC: {:X}, SP: {:X}, A: {:02X}, X: {:02X}, Y: {:02X}, [N:{}, V:{}, B{}, D{}, I{}, Z:{}, C:{}], H: {:08X}", _instructions[_currentOpcode].name, _currentOpcode, PC, SP, A, X, Y, diff --git a/src/Logger.cpp b/src/Logger.cpp index 5bd6e24..e297a00 100644 --- a/src/Logger.cpp +++ b/src/Logger.cpp @@ -6,7 +6,7 @@ #include #include -#include +#include namespace nes { @@ -19,7 +19,7 @@ namespace nes { return; } - std::string cycleString = std::format("[{:06d}] ", cycle); + std::string cycleString = fmt::format("[{:06d}] ", cycle); std::memcpy(_data.get() + _offset, cycleString.data(), cycleString.size()); _offset += cycleString.size(); diff --git a/src/Oam.cpp b/src/Oam.cpp new file mode 100644 index 0000000..894c135 --- /dev/null +++ b/src/Oam.cpp @@ -0,0 +1,144 @@ +// +// Created by Selim Mustafaev on 24.09.2023. +// + +#include "Oam.h" +#include "Ppu.h" + +#include + +namespace nes { + + Oam::Oam(Ppu *ppu): _ppu{ppu} { + } + + void Oam::setAddress(uint8_t address) { + _address = address; + } + + uint8_t Oam::read() const { + return reinterpret_cast(_data)[_address]; + } + + void Oam::write(uint8_t value) { + reinterpret_cast(_data)[_address] = value; + } + + void Oam::write(uint8_t address, uint8_t value) { + reinterpret_cast(_data)[address] = value; + } + + bool Oam::detectVisibleSprites(int16_t scanline, bool bigSprite) { + memset(_visibleSprites, 0xFF, MAX_SPRITES_PER_SCANLINE*sizeof(SpriteInfo)); + _visibleSpriteCount = 0; + + for (auto& shifter: _spriteShifters) { + shifter.reset(); + } + + size_t oamIndex = 0; + uint8_t spriteHeight = bigSprite ? 16 : 8; + bool spriteOverflow = false; + _spriteZeroVisible = false; + + while (oamIndex < OAM_SIZE && _visibleSpriteCount < MAX_SPRITES_PER_SCANLINE + 1) { + uint8_t yStart = _data[oamIndex].y; + uint8_t yEnd = yStart + spriteHeight; + + if(scanline >= yStart && scanline < yEnd) { + if(_visibleSpriteCount < MAX_SPRITES_PER_SCANLINE) { + _visibleSprites[_visibleSpriteCount] = _data[oamIndex]; + _visibleSpriteCount++; + if(oamIndex == 0) { + _spriteZeroVisible = true; + } + } else { + spriteOverflow = true; + } + } + + oamIndex++; + } + + return spriteOverflow; + } + + void Oam::loadShifters(int16_t scanline, bool bigSprite, bool patternSprite) { + for(uint8_t i = 0; i < _visibleSpriteCount; ++i) { + + auto sprite = _visibleSprites[i]; + bool flippedVertically = sprite.attr & 0x80; + bool flippedHorizontally = sprite.attr & 0x40; + bool isTopHalf = (scanline - sprite.y) < 8; + + uint8_t tileOffset = isTopHalf ? 0 : 1; + uint8_t rowOffset = flippedVertically ? (7 - scanline + sprite.y) : (scanline - sprite.y); + uint16_t patternAddress = 0; + + if(bigSprite) { + patternAddress = (sprite.id & 0x01) << 12 + | ((sprite.id & 0xFE) + tileOffset) << 4 + | (rowOffset & 0x07); + } else { + patternAddress = patternSprite << 12 + | sprite.id << 4 + | rowOffset; + } + + uint8_t patternBitsLo = _ppu->internalRead(patternAddress); + uint8_t patternBitsHi = _ppu->internalRead(patternAddress + 8); + + if(flippedHorizontally) { + patternBitsLo = flipByte(patternBitsLo); + patternBitsHi = flipByte(patternBitsHi); + } + + _spriteShifters[i].loadHighByte(patternBitsLo, patternBitsHi); + } + } + + uint8_t Oam::flipByte(uint8_t byte) const { + byte = (byte & 0xF0) >> 4 | (byte & 0x0F) << 4; + byte = (byte & 0xCC) >> 2 | (byte & 0x33) << 2; + byte = (byte & 0xAA) >> 1 | (byte & 0x55) << 1; + return byte; + } + + void Oam::updateShifters() { + for (size_t i = 0; i < _visibleSpriteCount; ++i) { + if(_visibleSprites[i].x > 0) { + _visibleSprites[i].x--; + } else { + _spriteShifters[i].shift(); + } + } + } + + Oam::PixelInfo Oam::getPixel() { + + uint8_t pattern, palette, priority; + _firstVisibleSpriteBeingRendered = false; + + for (size_t i = 0; i < _visibleSpriteCount; ++i) { + + if(_visibleSprites[i].x != 0) { + continue; + } + + pattern = _spriteShifters[i].getValue(0); + palette = (_visibleSprites[i].attr & 0x03) + SPRITE_PALETTE_OFFSET; + priority = (_visibleSprites[i].attr & 0x20) == 0; + + if(pattern > 0) { + _firstVisibleSpriteBeingRendered = (i == 0); + break; + } + } + + return std::make_tuple(pattern, palette, priority); + } + + bool Oam::spriteZeroBeingRendered() const { + return _spriteZeroVisible && _firstVisibleSpriteBeingRendered; + } +} \ No newline at end of file diff --git a/src/Oam.h b/src/Oam.h new file mode 100644 index 0000000..096710a --- /dev/null +++ b/src/Oam.h @@ -0,0 +1,61 @@ +// +// Created by Selim Mustafaev on 24.09.2023. +// + +#ifndef NES_OAM_H +#define NES_OAM_H + +#include "Shifter.h" +#include +#include +#include + +namespace nes { + + class Ppu; + + struct SpriteInfo { + uint8_t y = 0xFF; + uint8_t id = 0x00; + uint8_t attr = 0x00; + uint8_t x = 0xFF; + }; + + class Oam { + public: + static constexpr size_t OAM_SIZE = 64; + static constexpr size_t MAX_SPRITES_PER_SCANLINE = 8; + static constexpr uint8_t SPRITE_PALETTE_OFFSET = 4; + using PixelInfo = std::tuple; + + public: + explicit Oam(Ppu* ppu); + void setAddress(uint8_t address); + [[nodiscard]] uint8_t read() const; + void write(uint8_t value); + void write(uint8_t address, uint8_t value); + + public: + bool detectVisibleSprites(int16_t scanline, bool bigSprite); + void loadShifters(int16_t scanline, bool bigSprite, bool patternSprite); + void updateShifters(); + [[nodiscard]] PixelInfo getPixel(); + [[nodiscard]] bool spriteZeroBeingRendered() const; + + private: + [[nodiscard]] uint8_t flipByte(uint8_t byte) const; + + private: + Ppu* _ppu; + uint8_t _address = 0x00; + SpriteInfo _data[OAM_SIZE]; + SpriteInfo _visibleSprites[MAX_SPRITES_PER_SCANLINE]; + Shifter _spriteShifters[MAX_SPRITES_PER_SCANLINE]; + uint8_t _visibleSpriteCount = 0; + bool _spriteZeroVisible = false; + bool _firstVisibleSpriteBeingRendered = false; + }; + +} + +#endif //NES_OAM_H diff --git a/src/Ppu.cpp b/src/Ppu.cpp index 1c9ee90..9f0b96a 100644 --- a/src/Ppu.cpp +++ b/src/Ppu.cpp @@ -3,16 +3,17 @@ // #include "Ppu.h" -#include +#include namespace nes { Ppu::Ppu(): _column{}, _scanline{}, _status{}, _control{}, _mask{} { _frameBuffer = std::make_unique(SCREEN_WIDTH*SCREEN_HEIGHT); _nameTable = std::make_unique(1024); + _nameTable2 = std::make_unique(1024); _paletteTable = std::make_unique(32); _palette = std::make_unique(64); - _oamData = std::make_unique(64); + _oam = std::make_unique(this); _palette[0x00] = Pixel(84, 84, 84); _palette[0x01] = Pixel(0, 30, 116); @@ -100,6 +101,8 @@ namespace nes { if (_scanline == -1 && _column == 1) { _status.verticalBlank = 0; + _status.spriteOverflow = 0; + _status.spriteZeroHit = 0; } // Preloading some data ahead of time @@ -108,6 +111,9 @@ namespace nes { _bgPatternShifter.shift(); _bgAttribShifter.shift(); } + if(_mask.renderSprites && _column >= 2 && _column < 258) { + _oam->updateShifters(); + } prepareNextBgTile(_column); } @@ -131,22 +137,59 @@ namespace nes { } } - uint8_t colorIndex = 0; - uint8_t palette = 0; + if(_column == 257 && _scanline >= 0) { + _status.spriteOverflow = _oam->detectVisibleSprites(_scanline, _control.spriteSize); + } + + if(_column == 340) { + _oam->loadShifters(_scanline, _control.spriteSize, _control.patternSprite); + } + + uint8_t bgPattern = 0; + uint8_t bgPalette = 0; if(_mask.renderBackground) { - colorIndex = _bgPatternShifter.getValue(_fineX); - palette = _bgAttribShifter.getValue(_fineX); - Pixel pixel = getColor(palette, colorIndex); + bgPattern = _bgPatternShifter.getValue(_fineX); + bgPalette = _bgAttribShifter.getValue(_fineX); + } - _curPalette = palette; - _curColorIndex = colorIndex; + uint8_t fgPattern = 0; + uint8_t fgPalette = 0; + uint8_t priority = 0; - if(_column < SCREEN_WIDTH && _scanline < SCREEN_HEIGHT && _scanline >= 0) { - setPixel(_scanline, _column, pixel); + if(_mask.renderSprites) { + std::tie(fgPattern, fgPalette, priority) = _oam->getPixel(); + } + + uint8_t pattern = 0; + uint8_t palette = 0; + + if(bgPattern == 0 && fgPattern > 0) { + pattern = fgPattern; + palette = fgPalette; + } + else if(bgPattern > 0 && fgPattern == 0) { + pattern = bgPattern; + palette = bgPalette; + } + else if(bgPattern > 0 && fgPattern > 0) { + pattern = priority ? fgPattern : bgPattern; + palette = priority ? fgPalette : bgPalette; + + if(_mask.renderBackground && _mask.renderSprites && _oam->spriteZeroBeingRendered()) { + bool renderLeft = _mask.renderBackgroundLeft && _mask.renderSpritesLeft; + int16_t firstRow = renderLeft ? 0 : 8; + if(_column > firstRow && _column < 258) { + _status.spriteZeroHit = true; + } } } + if(_column < SCREEN_WIDTH && _scanline < SCREEN_HEIGHT && _scanline >= 0) { + Pixel pixel = getColor(palette, pattern); + setPixel(_scanline, _column, pixel); + } + _column++; if(_column >= 341) { _column = 0; @@ -178,7 +221,7 @@ namespace nes { _vRamAddress.value += _control.incrementMode ? 32 : 1; break; case OamData: - value = reinterpret_cast(_oamData.get())[_oamAddress]; + value = _oam->read(); break; } return value; @@ -219,10 +262,10 @@ namespace nes { _vRamAddress.value += _control.incrementMode ? 32 : 1; break; case OamAddress: - _oamAddress = value; + _oam->setAddress(value); break; case OamData: - reinterpret_cast(_oamData.get())[_oamAddress] = value; + _oam->write(value); break; default: break; @@ -262,7 +305,25 @@ namespace nes { } else if(address >= 0x2000 && address < 0x3F00) { address &= 0x0FFF; - return _nameTable[address & 0x03FF]; + if(_cartridge->mirroring() == Cartridge::Mirroring::Vertical) { + if (address >= 0x0000 && address <= 0x03FF) + return _nameTable[address & 0x03FF]; + if (address >= 0x0400 && address <= 0x07FF) + return _nameTable2[address & 0x03FF]; + if (address >= 0x0800 && address <= 0x0BFF) + return _nameTable[address & 0x03FF]; + if (address >= 0x0C00 && address <= 0x0FFF) + return _nameTable2[address & 0x03FF]; + } else { + if (address >= 0x0000 && address <= 0x03FF) + return _nameTable[address & 0x03FF]; + if (address >= 0x0400 && address <= 0x07FF) + return _nameTable[address & 0x03FF]; + if (address >= 0x0800 && address <= 0x0BFF) + return _nameTable2[address & 0x03FF]; + if (address >= 0x0C00 && address <= 0x0FFF) + return _nameTable2[address & 0x03FF]; + } } else if(address >= 0x3F00 && address < 0x4000) { address &= 0x1F; @@ -284,7 +345,25 @@ namespace nes { } else if(address >= 0x2000 && address < 0x3F00) { address &= 0x0FFF; - _nameTable[address & 0x03FF] = value; + if(_cartridge->mirroring() == Cartridge::Mirroring::Vertical) { + if (address >= 0x0000 && address <= 0x03FF) + _nameTable[address & 0x03FF] = value; + if (address >= 0x0400 && address <= 0x07FF) + _nameTable2[address & 0x03FF] = value; + if (address >= 0x0800 && address <= 0x0BFF) + _nameTable[address & 0x03FF] = value; + if (address >= 0x0C00 && address <= 0x0FFF) + _nameTable2[address & 0x03FF] = value; + } else { + if (address >= 0x0000 && address <= 0x03FF) + _nameTable[address & 0x03FF] = value; + if (address >= 0x0400 && address <= 0x07FF) + _nameTable[address & 0x03FF] = value; + if (address >= 0x0800 && address <= 0x0BFF) + _nameTable2[address & 0x03FF] = value; + if (address >= 0x0C00 && address <= 0x0FFF) + _nameTable2[address & 0x03FF] = value; + } } else if(address >= 0x3F00 && address < 0x4000) { address &= 0x1F; @@ -394,7 +473,7 @@ namespace nes { auto palettes = (uint32_t*)_paletteTable.get(); - return std::format("X:{:03d}, Y:{:03d}, V:{:d}, vR: [CX:{:02X}, CY:{:02X}, NX:{:d}, NY:{:d}, FY:{:d}], tR: [CX:{:02X}, CY:{:02X}, NX:{:d}, NY:{:d}, FY:{:d}], FX:{:d}, AL:{:d}, PI:{:02X}, CI:{:02X}, ASL:{:04X}, ASH:{:04X}", + return fmt::format("X:{:03d}, Y:{:03d}, V:{:d}, vR: [CX:{:02X}, CY:{:02X}, NX:{:d}, NY:{:d}, FY:{:d}], tR: [CX:{:02X}, CY:{:02X}, NX:{:d}, NY:{:d}, FY:{:d}], FX:{:d}, AL:{:d}, ASL:{:04X}, ASH:{:04X}", _column, _scanline, _status.verticalBlank, @@ -410,14 +489,11 @@ namespace nes { _tRamAddress.fineY, _fineX, _addressWriteInProgress, - _curPalette, - _curColorIndex, _bgAttribShifter._lo, _bgAttribShifter._hi); } void Ppu::writeOam(uint8_t address, uint8_t data) { - auto oamPtr = reinterpret_cast(_oamData.get()); - oamPtr[address] = data; + _oam->write(address, data); } } \ No newline at end of file diff --git a/src/Ppu.h b/src/Ppu.h index 7ae3c9f..a709c17 100644 --- a/src/Ppu.h +++ b/src/Ppu.h @@ -7,6 +7,7 @@ #include "Cartridge.h" #include "Shifter.h" +#include "Oam.h" #include #include @@ -97,13 +98,6 @@ namespace nes { uint8_t msb; }; - struct SpriteInfo { - uint8_t y; - uint8_t id; - uint8_t attr; - uint8_t x; - }; - public: Ppu(); bool tick(); @@ -114,13 +108,13 @@ namespace nes { void reset(); [[nodiscard]] std::string state() const; void writeOam(uint8_t address, uint8_t data); + uint8_t internalRead(uint16_t address); + void internalWrite(uint16_t address, uint8_t value); public: std::function onNewFrame; private: - uint8_t internalRead(uint16_t address); - void internalWrite(uint16_t address, uint8_t value); Pixel getColor(uint8_t palette, uint8_t pixel); void prepareNextBgTile(uint16_t column); void incrementScrollX(); @@ -147,19 +141,14 @@ namespace nes { private: std::unique_ptr _nameTable; + std::unique_ptr _nameTable2; std::unique_ptr _palette; std::unique_ptr _paletteTable; - - std::unique_ptr _oamData; - uint8_t _oamAddress; + std::unique_ptr _oam; private: std::unique_ptr _frameBuffer; Cartridge* _cartridge; - - private: // For debug - uint8_t _curPalette; - uint8_t _curColorIndex; }; } diff --git a/src/Shifter.cpp b/src/Shifter.cpp index 12acd0a..2bc2c69 100644 --- a/src/Shifter.cpp +++ b/src/Shifter.cpp @@ -14,15 +14,25 @@ namespace nes { _hi = (_hi & 0xFF00) | hi; } + void Shifter::loadHighByte(uint8_t lo, uint8_t hi) { + _lo = lo << 8; + _hi = hi << 8; + } + void Shifter::shift() { _lo <<= 1; _hi <<= 1; } - uint16_t Shifter::getValue(uint8_t offset) { + uint8_t Shifter::getValue(uint8_t offset) const { uint16_t mask = 0x8000 >> offset; bool loBit = _lo & mask; bool hiBit = _hi & mask; return (hiBit << 1) | loBit; } + + void Shifter::reset() { + _lo = 0; + _hi = 0; + } } \ No newline at end of file diff --git a/src/Shifter.h b/src/Shifter.h index 59a8992..14b1ed8 100644 --- a/src/Shifter.h +++ b/src/Shifter.h @@ -13,8 +13,10 @@ namespace nes { public: Shifter(); void load(uint8_t lo, uint8_t hi); + void loadHighByte(uint8_t lo, uint8_t hi); void shift(); - uint16_t getValue(uint8_t offset); + uint8_t getValue(uint8_t offset) const; + void reset(); public: uint16_t _lo;