Finised sprite rendering

This commit is contained in:
Selim Mustafaev 2023-09-24 23:14:34 +03:00
parent c1bd4c863f
commit 352c1588ae
12 changed files with 369 additions and 60 deletions

View File

@ -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)

View File

@ -3,7 +3,6 @@
#include "src/SdlKeyboardController.h"
#include <functional>
#include <chrono>
#include <thread>
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<SdlKeyboardController>());
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<int64_t>(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<std::chrono::nanoseconds>(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;

View File

@ -40,4 +40,8 @@ namespace nes {
return _chrRom[mappedAddress];
}
Cartridge::Mirroring Cartridge::mirroring() const {
return _header->flags.mirroring == 0 ? Mirroring::Horizontal : Mirroring::Vertical;
}
}

View File

@ -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<uint8_t[]> _romData;

View File

@ -5,7 +5,7 @@
#include "Cpu.h"
#include "Nes.h"
#include <iostream>
#include <format>
#include <fmt/format.h>
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,

View File

@ -6,7 +6,7 @@
#include <cstring>
#include <fstream>
#include <format>
#include <fmt/format.h>
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();

144
src/Oam.cpp Normal file
View File

@ -0,0 +1,144 @@
//
// Created by Selim Mustafaev on 24.09.2023.
//
#include "Oam.h"
#include "Ppu.h"
#include <cstring>
namespace nes {
Oam::Oam(Ppu *ppu): _ppu{ppu} {
}
void Oam::setAddress(uint8_t address) {
_address = address;
}
uint8_t Oam::read() const {
return reinterpret_cast<const uint8_t*>(_data)[_address];
}
void Oam::write(uint8_t value) {
reinterpret_cast<uint8_t*>(_data)[_address] = value;
}
void Oam::write(uint8_t address, uint8_t value) {
reinterpret_cast<uint8_t*>(_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;
}
}

61
src/Oam.h Normal file
View File

@ -0,0 +1,61 @@
//
// Created by Selim Mustafaev on 24.09.2023.
//
#ifndef NES_OAM_H
#define NES_OAM_H
#include "Shifter.h"
#include <cstdint>
#include <cstddef>
#include <tuple>
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<uint8_t, uint8_t, uint8_t>;
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

View File

@ -3,16 +3,17 @@
//
#include "Ppu.h"
#include <format>
#include <fmt/format.h>
namespace nes {
Ppu::Ppu(): _column{}, _scanline{}, _status{}, _control{}, _mask{} {
_frameBuffer = std::make_unique<Pixel[]>(SCREEN_WIDTH*SCREEN_HEIGHT);
_nameTable = std::make_unique<uint8_t[]>(1024);
_nameTable2 = std::make_unique<uint8_t[]>(1024);
_paletteTable = std::make_unique<uint8_t[]>(32);
_palette = std::make_unique<Pixel[]>(64);
_oamData = std::make_unique<SpriteInfo[]>(64);
_oam = std::make_unique<Oam>(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<uint8_t*>(_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<uint8_t*>(_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<uint8_t*>(_oamData.get());
oamPtr[address] = data;
_oam->write(address, data);
}
}

View File

@ -7,6 +7,7 @@
#include "Cartridge.h"
#include "Shifter.h"
#include "Oam.h"
#include <cstdint>
#include <memory>
@ -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<void(const Pixel*)> 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<uint8_t[]> _nameTable;
std::unique_ptr<uint8_t[]> _nameTable2;
std::unique_ptr<Pixel[]> _palette;
std::unique_ptr<uint8_t[]> _paletteTable;
std::unique_ptr<SpriteInfo[]> _oamData;
uint8_t _oamAddress;
std::unique_ptr<Oam> _oam;
private:
std::unique_ptr<Pixel[]> _frameBuffer;
Cartridge* _cartridge;
private: // For debug
uint8_t _curPalette;
uint8_t _curColorIndex;
};
}

View File

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

View File

@ -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;