started work on sprite rendering

This commit is contained in:
Lauchmelder 2022-03-06 23:31:52 +01:00
parent 6fe829d66c
commit ca729613c7
No known key found for this signature in database
GPG key ID: C2403C69D78F011D
10 changed files with 417 additions and 22 deletions

View file

@ -48,7 +48,17 @@ uint8_t Bus::Tick()
{
controllerPort.Tick();
uint8_t result = cpu.Tick();
uint8_t result = 0x00;
if (DMACyclesLeft == 0)
{
result = cpu.Tick();
}
else
{
DMATick();
result = DMACyclesLeft;
}
// 3 ppu ticks per cpu tick
ppu.Tick();
@ -62,6 +72,25 @@ uint8_t Bus::Tick()
return result;
}
void Bus::DMATick()
{
if (preDMACycles > 0)
{
preDMACycles--;
return;
}
if (DMALatch != 0)
{
Byte data = ReadCPU(((Word)DMAPage << 8) | (0xFF - DMACyclesLeft));
ppu.WriteRegister(0x2004, data);
DMACyclesLeft--;
}
DMALatch = 1 - DMALatch;
}
void Bus::PPUTick()
{
if (ppuClock == 0)
@ -125,6 +154,9 @@ Byte Bus::ReadCPU(Word addr)
{
switch (addr)
{
case 0x4014:
return 0x00;
case 0x4016:
case 0x4017:
return controllerPort.Read(addr);
@ -180,6 +212,12 @@ void Bus::WriteCPU(Word addr, Byte val)
{
switch (addr)
{
case 0x4014:
DMAPage = val;
DMACyclesLeft = 0xFF;
preDMACycles = 1 + (cpu.GetTotalCycles() % 2);
return;
case 0x4016:
controllerPort.Write(addr, val);
break;
@ -201,12 +239,9 @@ void Bus::WritePPU(Word addr, Byte val)
}
else if (0x2000 <= addr && addr < 0x3F00)
{
if(cartridge.MapCIRAM(addr))
if (cartridge.MapCIRAM(addr))
cartridge.WriteVRAM(addr, val);
if (val != 0x00)
volatile int jfkd = 3;
VRAM[addr & 0xFFF] = val;
}
else if (0x3F00 <= addr && addr < 0x4000)

View file

@ -47,6 +47,8 @@ public:
*/
uint8_t Tick();
void DMATick();
void PPUTick();
/**
@ -95,5 +97,10 @@ private:
Cartridge cartridge;
ControllerPort controllerPort;
Byte preDMACycles = 0;
Byte DMACyclesLeft = 0;
Byte DMAPage = 0;
Byte DMALatch = 0;
uint8_t ppuClock = 0;
};

View file

@ -13,7 +13,7 @@ add_executable(nesemu
"debugger/PPUWatcher.cpp"
"debugger/Disassembler.cpp"
"debugger/MemoryViewer.cpp"
"debugger/NametableViewer.cpp" "ControllerPort.cpp" "controllers/StandardController.cpp" "gfx/Input.cpp" "debugger/ControllerPortViewer.cpp" "gfx/Screen.cpp" "debugger/Palettes.cpp" "APU.cpp" "debugger/PatternTableViewer.cpp" "mappers/Mapper003.cpp" "mappers/Mapper001.cpp")
"debugger/NametableViewer.cpp" "ControllerPort.cpp" "controllers/StandardController.cpp" "gfx/Input.cpp" "debugger/ControllerPortViewer.cpp" "gfx/Screen.cpp" "debugger/Palettes.cpp" "APU.cpp" "debugger/PatternTableViewer.cpp" "mappers/Mapper003.cpp" "mappers/Mapper001.cpp" "debugger/OAMViewer.cpp")
target_include_directories(nesemu PRIVATE
mappers

View file

@ -107,6 +107,8 @@ public:
*/
void NMI();
uint64_t GetTotalCycles() { return totalCycles; }
private:
/**
* @brief Create a lookup table of instructions.

View file

@ -77,6 +77,9 @@ const std::vector<Color> PPU::colorTable = {
PPU::PPU(Bus* bus, Screen* screen) :
bus(bus), screen(screen), ppuctrl{ 0 }, ppustatus{ 0 }
{
OAM = std::vector<Byte>(64 * 4, 0);
secondaryOAM = std::vector<Byte>(8 * 4, 0);
sprites = std::vector<Sprite>(8, { 0 });
}
void PPU::Powerup()
@ -144,12 +147,17 @@ void PPU::Tick()
// This cycle resets the VBlankStarted flag
if (y == 261 && x == 1)
{
ppustatus.Flag.VBlankStarted = 0;
ppustatus.Flag.SpriteZeroHit = 0;
}
// Need to render
if (scanlineType == ScanlineType::Visible || scanlineType == ScanlineType::PreRender)
{
PerformRenderAction();
EvaluateBackgroundTiles();
EvaluateSprites();
if (x == 257)
{
current.CoarseX = temporary.CoarseX;
@ -169,20 +177,11 @@ void PPU::Tick()
if (x < 256 && y < 240)
{
Byte loBit = (loTile.Hi & 0x80) >> 7;
Byte hiBit = (hiTile.Hi & 0x80) >> 7;
Byte loAttrBit = (loAttribute.Hi & 0x80) >> 7;
Byte hiAttrBit = (hiAttribute.Hi & 0x80) >> 7;
Pixel bgPixel = GetBackgroundPixel();
Pixel spritePixel = GetSpritePixel();
uint8_t color = (hiBit << 1) | loBit;
uint8_t palette = (hiAttrBit << 1) | loAttrBit;
if (color == 0x00)
palette = 0x00;
uint8_t colorVal = Read(0x3F00 | (palette << 2) | color);
if(ppumask.Flag.ShowBackground)
screen->SetPixel(x, y, colorTable[colorVal]);
Color pixel = MultiplexPixel(bgPixel, spritePixel);
screen->SetPixel(x, y, pixel);
}
if (cycleType == CycleType::Fetching || cycleType == CycleType::PreFetching)
@ -217,6 +216,13 @@ Byte PPU::ReadRegister(Byte id)
addressLatch = 0;
break;
case 3:
break;
case 4:
data = ReadOAM(oamaddr++);
break;
case 5:
break;
@ -258,6 +264,14 @@ void PPU::WriteRegister(Byte id, Byte val)
ppustatus.Raw = val;
break;
case 3:
oamaddr = val;
break;
case 4:
WriteOAM(oamaddr++, val);
break;
// PPUADDR and PPUSCROLL both take 2 accesses to fully set
// When writing to them the address latch is switched. The latch
// determines whether the hi or lo byte should be written next
@ -311,6 +325,16 @@ void PPU::WriteRegister(Byte id, Byte val)
ppustatus.Flag.Unused = val & 0x1F;
}
Byte PPU::ReadOAM(Byte offset)
{
return OAM[offset] | OAMOverrideSignal;
}
void PPU::WriteOAM(Byte offset, Byte val)
{
OAM[offset] = val;
}
Byte PPU::Read(Word addr)
{
return bus->ReadPPU(addr);
@ -366,7 +390,7 @@ void PPU::UpdateState()
}
}
void PPU::PerformRenderAction()
void PPU::EvaluateBackgroundTiles()
{
if (cycleType == CycleType::Idle)
{
@ -443,3 +467,239 @@ void PPU::PerformRenderAction()
fineX = 0;
memoryAccessLatch = 1 - memoryAccessLatch;
}
void PPU::EvaluateSprites()
{
if (cycleType == CycleType::Idle)
{
currentlyEvaluatedSprite = 0x00;
return;
}
if (scanlineType != ScanlineType::PreRender)
{
OAMOverrideSignal = (x <= 64);
// Secondary OAM clear
if (x < 64)
{
secondaryOAM[x >> 1] = ReadOAM(0x00);
return;
}
// Sprite evaluation (just do it all at once)
if (x == 65)
{
freeSecondaryOAMSlot = 0x00;
Byte n;
for (n = 0; n < 64; n++)
{
if (oamaddr + 4 * n >= 64 * 4)
break;
Byte spriteYCoord = ReadOAM(oamaddr + 4 * n);
if (freeSecondaryOAMSlot >= 32)
break;
// Find free slot
secondaryOAM[freeSecondaryOAMSlot] = spriteYCoord;
if (y - spriteYCoord < 8 + ppuctrl.Flag.SpriteSize * 8) // Choose between 8x8 and 8x16 mode
{
secondaryOAM[freeSecondaryOAMSlot + 1] = ReadOAM(4 * n + 1);
secondaryOAM[freeSecondaryOAMSlot + 2] = ReadOAM(4 * n + 2);
secondaryOAM[freeSecondaryOAMSlot + 3] = ReadOAM(4 * n + 3);
freeSecondaryOAMSlot += 4;
}
}
Byte m = 0;
for (; n < 64; n++)
{
Byte spriteYCoord = ReadOAM(4 * n + m);
if (spriteYCoord < 240)
ppustatus.Flag.SpriteOverflow = 1;
else
m = (m + 1) % 4; // Correctly implement sprite overflow bug
}
return;
}
}
if (cycleType == CycleType::SpriteFetching)
{
oamaddr = 0;
// Sprite tile fetches
if (memoryAccessLatch == 1)
{
switch (fetchPhase)
{
case FetchingPhase::NametableByte: // Fetch garbage
nametableByte = Read(0x2000 | (current.Raw & 0x0FFF));
sprites[currentlyEvaluatedSprite].Counter = secondaryOAM[4 * currentlyEvaluatedSprite + 3];
sprites[currentlyEvaluatedSprite].FineX = 0;
fetchPhase = FetchingPhase::AttributeTableByte;
break;
case FetchingPhase::AttributeTableByte: // Fetch garbage
attributeTableByte = Read(0x23C0 | (current.Raw & 0x0C00) | ((current.CoarseY >> 2) << 3) | (current.CoarseX >> 2));
sprites[currentlyEvaluatedSprite].Latch = secondaryOAM[4 * currentlyEvaluatedSprite + 2];
fetchPhase = FetchingPhase::PatternTableLo;
break;
case FetchingPhase::PatternTableLo:
{
Byte spriteFineY = y - secondaryOAM[4 * currentlyEvaluatedSprite];
Byte tileNumber = secondaryOAM[4 * currentlyEvaluatedSprite + 1] + (spriteFineY >> 3);
sprites[currentlyEvaluatedSprite].Lo = Read(((Word)ppuctrl.Flag.SpritePatternTableAddr << 12) | (tileNumber << 4) + spriteFineY);
fetchPhase = FetchingPhase::PatternTableHi;
} break;
case FetchingPhase::PatternTableHi:
{
Byte spriteFineY = y - secondaryOAM[4 * currentlyEvaluatedSprite];
Byte tileNumber = secondaryOAM[4 * currentlyEvaluatedSprite + 1] + (spriteFineY >> 3);
sprites[currentlyEvaluatedSprite].Hi = Read(((Word)ppuctrl.Flag.SpritePatternTableAddr << 12) | (tileNumber << 4) + 8 + spriteFineY);
if (currentlyEvaluatedSprite * 4 >= freeSecondaryOAMSlot)
{
sprites[currentlyEvaluatedSprite].Lo = 0x00;
sprites[currentlyEvaluatedSprite].Hi = 0x00;
}
currentlyEvaluatedSprite++;
fetchPhase = FetchingPhase::NametableByte;
} break;
}
}
memoryAccessLatch = 1 - memoryAccessLatch;
}
}
Pixel PPU::GetBackgroundPixel()
{
Pixel returnValue{ 0 };
returnValue.color = 0x00;
returnValue.palette = 0;
if (!ppumask.Flag.ShowBackground)
return returnValue;
Byte loBit = (loTile.Hi & 0x80) >> 7;
Byte hiBit = (hiTile.Hi & 0x80) >> 7;
Byte loAttrBit = (loAttribute.Hi & 0x80) >> 7;
Byte hiAttrBit = (hiAttribute.Hi & 0x80) >> 7;
returnValue.color = (hiBit << 1) | loBit;
returnValue.palette = (hiAttrBit << 1) | loAttrBit;
if (returnValue.color == 0x00)
returnValue.palette = 0x00;
return returnValue;
}
Pixel PPU::GetSpritePixel()
{
for (Sprite& sprite : sprites)
{
if (sprite.Counter != 0)
{
sprite.Counter--;
continue;
}
if (sprite.FineX != 8)
sprite.FineX++;
}
Pixel returnValue{ 0 };
returnValue.color = 0x00;
returnValue.palette = 4;
returnValue.priority = 1;
if (!ppumask.Flag.ShowSprites)
return returnValue;
bool firstIteration = true;
for (Sprite& sprite : sprites)
{
// Sprite is inacitve
if (sprite.Counter != 0 || sprite.FineX >= 8)
continue;
// If sprite is active, determine the current pixel
Byte loBit = (sprite.Hi & 0x80) >> 7;
Byte hiBit = (sprite.Hi & 0x80) >> 7;
uint8_t color = (hiBit << 1) | loBit;
if (color == 0x00)
{
firstIteration = false;
continue;
}
uint8_t palette = 4 + (sprite.Latch & 0x3);
if (color == 0x00)
palette = 0x00;
returnValue.color = color;
returnValue.palette = palette;
returnValue.priority = (sprite.Latch >> 5) & 0x1;
returnValue.isZeroSprite = firstIteration;
break;
}
return returnValue;
}
Color PPU::MultiplexPixel(Pixel background, Pixel sprite)
{
if (background.color == 0)
{
if (sprite.color == 0)
return colorTable[Read(0x3F00)];
else
return colorTable[Read(0x3F00 | (sprite.palette << 2) | sprite.color)];
}
else
{
if (sprite.color == 0)
return colorTable[Read(0x3F00 | (background.palette << 2) | background.color)];
else
{
// Sprite Zero hit detection
if (sprite.isZeroSprite)
{
// All of the conditions that make sprite zero hits not evaluate
if (!(
(!ppustatus.Flag.SpriteZeroHit) &&
(x == 255) &&
(!ppumask.Flag.ShowBackground || !ppumask.Flag.ShowSprites) &&
((!ppumask.Flag.SpriteOnLeft || !ppumask.Flag.BackgroundOnLeft) && 0 <= x && x <= 7)
)) ppustatus.Flag.SpriteZeroHit = 1;
}
if(sprite.priority == 0)
return colorTable[Read(0x3F00 | (sprite.palette << 2) | sprite.color)];
else
return colorTable[Read(0x3F00 | (background.palette << 2) | background.color)];
}
}
}

View file

@ -55,12 +55,29 @@ union ShiftRegister
Word Raw;
};
struct Sprite
{
Byte Lo, Hi;
Byte Latch;
Byte Counter;
Byte FineX;
};
struct Pixel
{
Byte color;
Byte palette;
Byte priority;
bool isZeroSprite;
};
/**
* @brief The PPU of the NES.
*/
class PPU
{
friend class PPUWatcher;
friend class OAMViewer;
public:
static const std::vector<Color> colorTable;
@ -95,6 +112,9 @@ public:
*/
void WriteRegister(Byte id, Byte val);
Byte ReadOAM(Byte offset);
void WriteOAM(Byte offset, Byte val);
/**
* @brief Check whether the PPU finished rendering a frame.
* Returns true if the VBlankStart cycle was hit previously. The function resets
@ -114,7 +134,13 @@ private:
void Write(Word addr, Byte val);
void UpdateState();
void PerformRenderAction();
void EvaluateBackgroundTiles();
void EvaluateSprites();
Pixel GetBackgroundPixel();
Pixel GetSpritePixel();
Color MultiplexPixel(Pixel background, Pixel sprite);
private: // Registers
@ -171,6 +197,7 @@ private: // Registers
} ppuscroll;
Address ppuaddr;
Byte oamaddr;
VRAMAddress current{ 0 };
VRAMAddress temporary{ 0 };
@ -190,6 +217,13 @@ private: // Registers
ShiftRegister hiAttribute{ 0 };
ShiftRegister loAttribute{ 0 };
std::vector<Byte> OAM;
std::vector<Byte> secondaryOAM;
std::vector<Sprite> sprites;
Byte OAMOverrideSignal = 0x00;
Byte freeSecondaryOAMSlot = 0x00;
Byte currentlyEvaluatedSprite = 0x00;
private:
ScanlineType scanlineType;
CycleType cycleType;

View file

@ -10,6 +10,7 @@
#include "Disassembler.hpp"
#include "MemoryViewer.hpp"
#include "NametableViewer.hpp"
#include "OAMViewer.hpp"
#include "PatternTableViewer.hpp"
#include "ControllerPortViewer.hpp"
#include "Palettes.hpp"
@ -27,6 +28,7 @@ Debugger::Debugger(Bus* bus) :
windows.push_back(new MemoryViewer(this, bus));
windows.push_back(new NametableViewer(this, bus));
windows.push_back(new OAMViewer(this, &bus->ppu));
windows.push_back(new PatternTableViewer(this, bus->cartridge.GetMapper()));
windows.push_back(new ControllerPortViewer(this, &bus->controllerPort));
windows.push_back(new Palettes(this, bus));

View file

@ -0,0 +1,33 @@
#include "OAMViewer.hpp"
#include "../PPU.hpp"
#include <imgui/imgui.h>
OAMViewer::OAMViewer(Debugger* debugger, PPU* ppu) :
DebugWindow("OAM Viewer", debugger), ppu(ppu)
{
}
void OAMViewer::OnRender()
{
if (!ImGui::Begin("OAM Viewer", &isOpen))
{
ImGui::End();
return;
}
char label[sizeof("Sprite 00")];
for (int i = 0; i < 64; i++)
{
std::sprintf(label, "Sprite %02d", i);
if (ImGui::CollapsingHeader(label))
{
ImGui::Text("Y pos : %02X", ppu->OAM[4 * i + 0]);
ImGui::Text("Tile : %02X", ppu->OAM[4 * i + 1]);
ImGui::Text("Attribute: %02X", ppu->OAM[4 * i + 2]);
ImGui::Text("X pos : %02X", ppu->OAM[4 * i + 3]);
}
}
ImGui::End();
}

View file

@ -0,0 +1,17 @@
#pragma once
#include "DebugWindow.hpp"
class PPU;
class OAMViewer :
public DebugWindow
{
public:
OAMViewer(Debugger* debugger, PPU* ppu);
virtual void OnRender() override;
private:
PPU* ppu;
};

View file

@ -121,6 +121,11 @@ void PPUWatcher::OnRender()
ImGui::TableNextColumn();
ImGui::Text("$%04X", ppu->ppuctrl.Flag.BackgrPatternTableAddr ? 0x1000 : 0x0000);
ImGui::TableNextColumn();
ImGui::Text("Sprite Size");
ImGui::TableNextColumn();
ImGui::Text(ppu->ppuctrl.Flag.SpriteSize ? "8x16" : "8x8");
ImGui::TableNextColumn();
ImGui::Text("Master/Slave");
ImGui::TableNextColumn();